However, I saw interested people, and I think it’s the right time to write a blog post about some design choices. I’ll start off with luminance and I’ll speak about spectra in another blog post because I truly think spectra starts to become very interesting (I have my own shading language bundled up within, which is basically GLSL on steroids).
luminance is a library that I wrote, historically, in Haskell. You can find the package here if you’re interested – careful though, it’s dying and the Rust version has completely drifted path with it. Nevertheless, when I ported the library to Rust, I imported the same “way of programming” that I had·have in Haskell – besides the allocation scheme; remember, I’m a demoscener, I do care a lot about performance, CPU usage, cache friendliness and runtime size. So the Rust luminance crate was made to be hybrid: it has the cool functional side that I imported from my Haskell codebase, and the runtime performance I wanted that I had when I wrote my two first 64k in C++11. I had to remove and work around some features that only Haskell could provide, such as higher-kinded types, type families, functional dependencies, GADTs and a few other things such as existential quantification (trait objects saved me here, even though I don’t use them that much in luminance now).
I have to admit, I dithered a lot about the scope of luminance — both in Haskell and Rust. At first, I thought that it’d be great to have a “base” crate, hosting common and abstracted code, and “backend” crates, implementing the abstract interface. That would enable me to have several backends – OpenGL, Vulkan, Metal, a software implementation, something for Amiigaaaaaaa, etc. Though, time has passed, and now, I think it’s:
The idea is that if you need to be very low-level on the graphics stack of your application, you’re likely to know what you are doing. And then, your needs will be very precise and well-defined. You might want very specific piece of code to be available, related to a very specific technology. That’s the reason why abstracting over very low-level code is not a good path to me: you need to expose as most as posssible the low-level interface. That’s the goal of luminance: exposing OpenGL’s interface in a stateless, bindless and typesafe way, with no or as minimal as possible runtime overhead.
More reading here.
Today, luminance is almost stable – it still receives massive architecture redesign from time to
time, but it’ll hit the 1.0.0 release soon. As discussed with kvark lately, luminance is not
about the same scope as gfx’s one. The goals of luminance are:
std nor core).To achieve that, luminance is written with several aspects in mind:
unsafe).gl dependency, of course).luminance has received more attention lately, and I think it’s a good thing to talk about how to use it. I’ll add examples on github and its docs.rs online documentation.
I’m going to do that like a tutorial. It’s easier to read and you can test the code in the same time. Let’s render a triangle!
Note: keep in mind that you need a nightly compiler to compile luminance.
I’ll do everything from scratch with you. I’ll work in /tmp:
$ cd /tmp
First thing first, let’s setup a lumitest Rust binary project:
$ cargo init --bin lumitest
Created binary (application) project
$ cd lumitest
Let’s edit our Cargo.toml to use luminance. We’ll need two crates:
At the time of writing, corresponding versions are luminance-0.23.0 and luminance-glfw-0.3.2.
Have the following [dependencies] section
[dependencies]
luminance = "0.23.0"
luminance-glfw = "0.3.2"
$ cargo check
Everything should be fine at this point. Now, let’s step in in writing some code.
extern crate luminance;
extern crate luminance_glfw;
use luminance_glfw::{Device, WindowDim, WindowOpt};
const SCREEN_WIDTH: u32 = 960;
const SCREEN_HEIGHT: u32 = 540;
fn main() {
let rdev = Device::new(WindowDim::Windowed(SCREEN_WIDTH, SCREEN_HEIGHT), "lumitest", WindowOpt::default());
}
The main function creates a Device
that is responsible in holding the windowing stuff for us.
Let’s go on:
match rdev {
Err(e) => {
eprintln!("{:#?}", e);
::std::process::exit(1);
}
Ok(mut dev) => {
println!("let’s go!");
}
}
This block will catch any Device errors and will print them to stderr if there’s any.
Let’s write the main loop:
'app: loop {
for (_, ev) in dev.events() { // the pair is an interface mistake; it’ll be removed
match ev {
WindowEvent::Close | WindowEvent::Key(Key::Escape, _, Action::Release, _) => break 'app,
_ => ()
}
}
}
This loop runs forever and will exit if you hit the escape key or quit the application.
Now, the most interesting thing: rendering the actual triangle! You will need a few things:
type Position = [f32; 2];
type RGB = [f32; 3];
type Vertex = (Position, RGB);
const TRIANGLE_VERTS: [Vertex; 3] = [
([-0.5, -0.5], [0.8, 0.5, 0.5]), // red bottom leftmost
([-0., 0.5], [0.5, 0.8, 0.5]), // green top
([0.5, -0.5], [0.5, 0.5, 0.8]) // blue bottom rightmost
];
Position, Color and Vertex define what a vertex is. In our case, we use a 2D position and a
RGB color.
You have a lot of choices here to define the type of your vertices. In theory, you can choose any type you want. However, it must implement the
Vertextrait. Have a look at the implementors that already exist for a faster start off!
Important: do not confuse between
[f32; 2]and(f32, f32). The former is a single 2D vertex component. The latter is two 1D components. It’ll make a huge difference when writing shaders.
The TRIANGLE_VERTS is a constant array with three vertices defined in it: the three vertices of
our triangle. Let’s pass those vertices to the GPU with the Tess
type:
// at the top location
use luminance::tess::{Mode, Tess, TessVertices};
// just above the main loop
let triangle = Tess::new(Mode::Triangle, TessVertices::Fill(&TRIANGLE_VERTS), None);
This will pass the TRIANGLE_VERTS vertices to the GPU. You’re given back a triangle object. The
Mode is a hint object that
states how vertices must be connected to each other. TessVertices lets you slice your vertices –
this is typically enjoyable when you use a mapped buffer that contains a dynamic number of vertices.
We’ll need a shader to render that triangle. First, we’ll place its source code in data:
$ mkdir data
Paste this in data/vs.glsl:
layout (location = 0) in vec2 co;
layout (location = 1) in vec3 color;
out vec3 v_color;
void main() {
gl_Position = vec4(co, 0., 1.);
v_color = color;
}
Paste this in data/fs.glsl:
in vec3 v_color;
out vec4 frag;
void main() {
frag = vec4(v_color, 1.);
}
And add this to your main.rs:
const SHADER_VS: &str = include_str!("../data/vs.glsl");
const SHADER_FS: &str = include_str!("../data/fs.glsl");
Note: this is not a typical workflow. If you’re interested in shaders, have a look at how I do it in spectra. That is, hot reloading it via SPSL (Spectra Shading Language), which enables to write GLSL modules and compose them in a single file but just writing functions. The functional programming style!
Same thing as for the tessellation, we need to pass the source to the GPU’s compiler to end up with a shader object:
// add this at the top of your main.rs
use luminance::shader::program::Program;
// below declaring triangle
let (shader, warnings) = Program::::from_strings(None, SHADER_VS, None, SHADER_FS).unwrap();
for warning in &warnings {
eprintln!("{:#?}", warning);
}
Finally, we need to tell luminance in which framebuffer we want to make the render. It’s simple: to the default framebuffer, which ends up to be… your screen’s back buffer! This is done this way with luminance:
use luminance::framebuffer::Framebuffer;
let screen = Framebuffer::default([SCREEN_WIDTH, SCREEN_HEIGHT]);
And we’re done for the resources. Let’s step in the actual render now.
luminance’s approach to render is somewhat not very intuitive yet very simple and very efficient: the render pipeline is explicitly defined by the programmer in Rust, on the fly. That means that you must express the actual state the GPU must have for the whole pipeline. Because of the nature of the pipeline, which is an AST (Abstract Syntax Tree), you can batch sub-parts of the pipeline (we call such parts nodes) and you end up with minimal GPU state switches. The theory is as following:
pipeline
function that introduces the concept of shading things to a framebuffer.
Tess
objects.That deep nesting enables you to batch your objects on a very fine granularity. Also, notice that
the functions are not about slices of Tess or hashmaps of Program. The allocation scheme is
completely ignorant about how the data is traversed, which is good: you decide. If you need to
borrow things on the fly in a shading gate, you can.
Let’s get things started:
use luminance::pipeline::{entry, pipeline};
entry(|_| {
pipeline(&screen, [0., 0., 0., 1.], |shd_gate| {
shd_gate.shade(&shader, |rdr_gate, _| {
rdr_gate.render(None, true, |tess_gate| {
let t = ▵
tess_gate.render(t.into());
});
});
});
});
We just need a final thing now: since we render to the back buffer of the screen, if we want to see
anything appear, we need to swap the buffer chain so that the back buffer become the front buffer
and the front buffer become the back buffer. This is done by wrapping our render code in the
Device::draw
function:
dev.draw(|| {
entry(|_| {
pipeline(&screen, [0., 0., 0., 1.], |shd_gate| {
shd_gate.shade(&shader, |rdr_gate, _| {
rdr_gate.render(None, true, |tess_gate| {
let t = ▵
tess_gate.render(t.into());
});
});
});
});
});
You should see this:

As you can see, the code is pretty straightforward. Let’s get deeper, and let’s kick some time in!
use std::time::Instant;
// before the main loop
let t_start = Instant::now();
// in your main loop
let t_dur = t_start.elapsed();
let t = (t_dur.as_secs() as f64 + t_dur.subsec_nanos() as f64 * 1e-9) as f32;
We have the time. Now, we need to pass it down to the GPU (i.e. the shader). luminance handles that kind of things with two concepts:
Uniforms are a good match when you want to send data to a specific shader, like a value that customizes the behavior of a shading algorithm.
Because buffers are shared, you can use buffers to share data between shader, leveraging the need to pass the data to all shader by hand – you only pass the index to the buffer that contains the data.
We won’t conver the buffers for this time.
Because of type safety, luminance requires you to state which types the uniforms the shader contains are. We only need the time, so let’s get this done:
// you need to alter this import
use luminance::shader::program::{Program, ProgramError, Uniform, UniformBuilder, UniformInterface, UniformWarning};
struct TimeUniform(Uniform);
impl UniformInterface for TimeUniform {
fn uniform_interface(builder: UniformBuilder) -> Result<(Self, Vec), ProgramError> {
// this will fail if the "t" variable is not used in the shader
//let t = builder.ask("t").map_err(ProgramError::UniformWarning)?;
// I rather like this one: we just forward up the warning and use the special unbound uniform
match builder.ask("t") {
Ok(t) => Ok((TimeUniform(t), Vec::new())),
Err(e) => Ok((TimeUniform(builder.unbound()), vec![e]))
}
}
}
The UniformBuilder::unbound
is a simple function that gives you any uniform you want: the resulting uniform object will just do
nothing when you pass values in. It’s a way to say “— Okay, I don’t use that in the shader yet, but
don’t fail, it’s not really an error”. Handy.
And now, all the magic: how do we access that uniform value? It’s simple: via types! Have you
noticed the type of our Program? For the record:
let (shader, warnings) = Program::::from_strings(None, SHADER_VS, None, SHADER_FS).unwrap();
See the type is parametered with three type variables:
Vertex, our own type – is for the input of the shader program.You guessed it: we need to change the third parameter from () to TimeUniform:
let (shader, warnings) = Program::::from_strings(None, SHADER_VS, None, SHADER_FS).unwrap();
And that’s all. Whenever you shade with a ShaderGate, the type of the shader object is being
inspected, and you’re handed with the uniform interface:
shd_gate.shade(&shader, |rdr_gate, uniforms| {
uniforms.0.update(t);
rdr_gate.render(None, true, |tess_gate| {
let t = ▵
tess_gate.render(t.into());
});
});
Now, change your fragment shader to this:
in vec3 v_color;
out vec4 frag;
uniform float t;
void main() {
frag = vec4(v_color * vec3(cos(t * .25), sin(t + 1.), cos(1.25 * t)), 1.);
}
And enjoy the result! Here’s the gist
that contains the whole main.rs.
lib.rs / main.rs files, that get rendered as front page on docs.rs. This is important
because we want people to have nice onboardings when hitting the official documentations of our
crates.README.md, that gets automatically rendered as HTML when you end up on
the project’s GitHub page. This page should provide as many as possible hints and information
about what the project is and how to use it.However, sometimes, I just don’t want to bother writing twice the same thing and honestly, I don’t really know whether the README file is a good place to write an onboarding section, since that should also be included in your Rust documentation on docs.rs.
So… I’ve been thinking of a way to fix that, without really investing too much time in it. But lately, I came to the realization that I often have this pattern:
lib.rs or main.rs. For instance, I’m pretty proud
of the onboarding documentation of warmy. It’s exhaustive, easy for newcomers, it has plenty
of links and explanations and it renders pretty nice on docs.rs.Here, (2.) is a manual and annoying task: I open my editor, make a block selection of the documentation, store it in a buffer, apply a smart macro on it to remove the Rust annotation and then paste the result in the README after having purged it from the previous documentation… That’s tedious and not very interesting.
cargo sync-readme to the rescue!So, yesterday, I decided to start working on a small tool that would automate all this for me. The idea is the following:
cargo sync-readme synchronizes your README (the file specified by the readme key in your
Cargo.toml, or just README.md by default) with the entrypoint of your library or binary
crate (by default, lib.rs or main.rs, or what is defined at the path key in your
manifest).<!-- cargo-sync-readme --> marker must lie on a single line where you want the
documentation to be inserted in your README.<!-- cargo-sync-readme start> and
<!-- cargo-sync-readme end -->.This is really all you have to do. cargo sync-readme will take care of the rest for you. There’re
two hypothesis that the command requires to be true, though:
//! annotation to write your documentation. This is currently how
cargo sync-readme works. It doesn’t have to be solely using this annotation on longer term,
but currently, this is the only annotation supported. More can be added if needed.Basically, insert the marker once, and run cargo sync-readme. Every time you change your Rust
documentation, just call cargo sync-readme once again to synchronize the documentation in your
README file.
Currently, cargo sync-readme doesn’t work with workspace crates. You will have to go into each of
the workspace’s members to run cargo sync-readme if you want to synchronize their respective
READMEs.
cargo sync-readme is already available on crates.io for you to use. You can install it as a
development tool with the following command:
cargo install cargo-sync-readme
Disclaimer: after having published
cargo-sync-readme, I was told that there is already another cargo plugin, cargo-readme, that already does that. Indeed, that crate does more or less the same job. However, the way it does it is very different. First, it uses a template file whilecargo-sync-readmelets you use your README file without having to depend on a template. Also, cargo-readme has special semantics in its template (like {{crate_name}}, etc.) whilecargo-sync-readmeis simpler and just requires a single marker. To sum up:cargo-readmeis heavier and is likely to require you to break your file into two separate files but contains more customization options whilecargo-sync-readmeonly requires a single line in your README and will synchronize from within that same file.
Feel free to comment, open issues, drink beers, share that awesome bear driving a sausage podracer and most of all… keep the vibes!
]]>The idea is simple: you are writing a crate and want to expose an API to people. You want them to
know which type they can use with a given operation (let’s call it update). However, the actual
implementation of this update function is not performed directly by your API but is deferred to a
backend implementation. Some people usually like to do that with several crates; in my case, I
really don’t care and let’s think in terms of types / modules instead.
There are several ways to do this. Let’s start with a nobrainer.
use std::marker::PhantomData;
// This type serves as to reify typing information at runtime and also serves as “public contract”.
// What it means is that this type gives you all the possible logical types you can use.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Type {
Int,
UInt,
Float,
Bool
}
// We will use a very small abstraction to represent the update function (and the backend).
// Var<B, T> is just a variable of type T represented in the backend B.
#[derive(Debug)]
pub struct Var<B, T> where B: ?Sized {
name: String,
_b: PhantomData<*const B>,
_t: PhantomData<*const T>,
}
impl<B, T> Var<B, T> {
// Just create a variable of a given name, whatever its type and backend.
pub fn new(name: &str) -> Self {
Var {
name: name.to_owned(),
_b: PhantomData,
_t: PhantomData,
}
}
}
impl<B, T> Var<B, T> where B: Backend, T: Interface {
// Update the value of the variable.
//
// For simplicity, I don’t include a mutable reference to the backend, because that is not the
// topic of this blog article. However, you might want to if you want to be able to, for instance,
// change the value in a map stored in the backend, for instance. But whatever.
pub fn update(&mut self, value: &T) {
B::update(self, value);
}
}
// The interface (i.e. public API) trait. This serves as a “anything implementing that can be used
// with the API.” It simply associates a Type that is, if you remember, an exhaustive list of types
// that can be used (and reified at runtime).
trait Interface {
const TY: Type;
}
impl Interface for i32 {
const TY: Type = Type::Int;
}
impl Interface for u32 {
const TY: Type = Type::UInt;
}
impl Interface for f32 {
const TY: Type = Type::Float;
}
impl Interface for bool {
const TY: Type = Type::Bool;
}
// The backend (i.e. private implementations) trait. An implementation should implement this trait
// to implement the actual update of a variable for a given context / backend. Notice the Interface
// constraint on the T type parameter.
trait Backend {
fn update<T>(var: &mut Var<Self, T>, value: &T) where T: Interface;
}
impl Backend for () {
fn update<T>(var: &mut Var<Self, T>, _: &T) where T: Interface {
// We can reify the type of the variable at runtime thanks to the associated constant value.
// However, you can see that we have no way to actually inspect the value. This is due to the
// fact our update function is universally quantified on T: we cannot observe T (i.e. know its
// exact type) from within the implementation. That might be a problem.
match T::TY {
Type::Int => println!("setting {} to int", var.name),
Type::UInt => println!("setting {} to unsigned int", var.name),
Type::Float => println!("setting {} to float", var.name),
Type::Bool => println!("setting {} to bool", var.name),
}
}
}
Let’s dig in. The idea of this solution is to have a trait, Interface, that is used to create a
set of types that can be used in the API with the update function on the Var type. The
implementation is deferred to a backend via the Backend trait, that contains the interface of
the implementation. Basically, the Var::update function will select at compile-time which
backend to use, that will in its turn observe the type of the variable at runtime — see the
match block. This is not ideal as we will have branching. We would like a better way to do it.
Instead of dynamically dispatching the types of the variable, we can play around with it at compile-time. That requires some changes but in the end it’s pretty clear what we need to do:
use std::marker::PhantomData;
// We don’t need the Type enum anymore so I just removed it. However, even with that solution, we
// could still want to keep it around.
#[derive(Debug)]
pub struct Var<B, T> where B: ?Sized, T: ?Sized {
name: String,
_b: PhantomData<*const B>,
_t: PhantomData<*const T>,
}
impl<B, T> Var<B, T> {
pub fn new(name: &str) -> Self {
Var {
name: name.to_owned(),
_b: PhantomData,
_t: PhantomData,
}
}
}
impl<B, T> Var<B, T> where B: Backend, T: Interface {
pub fn update(&mut self, value: &T) {
// Notice how now, we use the update method from the Interface and not the Backend! Important
// change, as you will see.
T::update(self, value)
}
}
// The interface trait loses the associated constant and gets a method, update. This method must
// work for any backend (i.e. types that implement Backend), since it’s our public facing trait.
trait Interface {
fn update<B>(var: &mut Var<B, Self>, value: &Self) where B: Backend;
}
// We can then implement it for all our types by selecting manually the update_* method we want.
impl Interface for i32 {
fn update<B>(var: &mut Var<B, Self>, value: &Self) where B: Backend {
B::update_i32(var, value);
}
}
impl Interface for u32 {
fn update<B>(var: &mut Var<B, Self>, value: &Self) where B: Backend {
B::update_u32(var, value);
}
}
impl Interface for f32 {
fn update<B>(var: &mut Var<B, Self>, value: &Self) where B: Backend {
B::update_f32(var, value);
}
}
impl Interface for bool {
fn update<B>(var: &mut Var<B, Self>, value: &Self) where B: Backend {
B::update_bool(var, value);
}
}
// The trait backend is now a list of methods that will be available to the Interface’s
// implementors.
trait Backend {
fn update_i32(var: &mut Var<Self, i32>, value: &i32);
fn update_u32(var: &mut Var<Self, u32>, value: &u32);
fn update_f32(var: &mut Var<Self, f32>, value: &f32);
fn update_bool(var: &mut Var<Self, bool>, value: &bool);
}
/// Implementing the backend trait now gives us a power we didn’t have back then in solution 1: we
// now can observe the type of the variable and, then, we can actually do useful things with it, as
// displaying it!
impl Backend for () {
fn update_i32(var: &mut Var<Self, i32>, value: &i32) {
println!("setting {} to int {}", var.name, value);
}
fn update_u32(var: &mut Var<Self, u32>, value: &u32) {
println!("setting {} to unsigned int {}", var.name, value);
}
fn update_f32(var: &mut Var<Self, f32>, value: &f32) {
println!("setting {} to float {}", var.name, value);
}
fn update_bool(var: &mut Var<Self, bool>, value: &bool) {
println!("setting {} to bool {}", var.name, value);
}
}
We change the definition of the Backend trait to have all the functions dispatched statically.
Then, using the Interface trait, we now have one information we didn’t have in the first example:
the actual, concrete type of the variable. We can then call the right function from the Backend
trait.
To sum up, because all of this is starting to be a bit confusing:
Interface trait is the trait used to restrict the public API (i.e. which types can be
used publicly).Backend trait is the trait to implement when providing the actual implementation.However, if we add more types, that solution won’t scale easily. The problem is that we have the
typing information in several places (at the impl level and in a static list in a trait). It would
be much easier if we could, somehow, force an impl to exist in another trait. Basically, I want to
remove those update_* and use impls instead.
I have no idea how to call that way of doing but I like to think of it as inferred constraints.
The idea is almost the same as the second solution but instead of declaring the list of functions
that can be used in the implementation of the Interface trait, we will just create a generic
dependency between the Interface trait and Backend. The advantage will also be that we don’t
have to worry about the name of the function anymore, since it will be polymorphic.
Let’s go.
use std::marker::PhantomData;
#[derive(Debug)]
pub struct Var<B, T> where B: ?Sized, T: ?Sized {
name: String,
_b: PhantomData<*const B>,
_t: PhantomData<*const T>,
}
impl<B, T> Var<B, T> {
pub fn new(name: &str) -> Self {
Var {
name: name.to_owned(),
_b: PhantomData,
_t: PhantomData,
}
}
}
// Wouhouh, some changes here. We go back to the first solution by using the update method of the
// Backend trait, but notice how we bind the Backend trait to the Interface by using Backend<T>.
impl<B, T> Var<B, T> where T: Interface, B: Backend<T> {
pub fn update(&mut self, value: &T) {
B::update(self, value)
}
}
// This trait is now a bit dumb and even dangerous as it’s very easy to implement it. In a perfect
// Rust world, we would have sealed traits — i.e. we could only implement it from the inside of the
// current crate.
trait Interface {}
impl Interface for i32 {}
impl Interface for u32 {}
impl Interface for f32 {}
impl Interface for bool {}
// The backend trait now has an associated type parameter that represents the variable type. What
// it means is that we will be able to implement the update function for all types of our choices…
// and still observe the type, since from inside the update function, it’s not universally
// quantified anymore!
trait Backend<T> {
fn update(var: &mut Var<Self, T>, value: &T);
}
impl Backend<i32> for () {
fn update(var: &mut Var<Self, i32>, value: &i32) {
println!("setting {} to int {}", var.name, value);
}
}
impl Backend<u32> for () {
fn update(var: &mut Var<Self, u32>, value: &u32) {
println!("setting {} to unsigned int {}", var.name, value);
}
}
impl Backend<f32> for () {
fn update(var: &mut Var<Self, f32>, value: &f32) {
println!("setting {} to float {}", var.name, value);
}
}
impl Backend<bool> for () {
fn update(var: &mut Var<Self, bool>, value: &bool) {
println!("setting {} to bool {}", var.name, value);
}
}
This solution is quite interesting because of the use of the type parameter in the Backend trait.
It enables you to implement the Backend trait and still observe (i.e. know) the type of the
variable you’re playing with. Compare with the first solution, where we were completely generic on
the T type.
In luminance, I use the last solution to allow a clear distinction between a set of public types
and a set of matching implementations. Notice in the last solution the Interface trait, which is
now reduced to something pretty dumb. It would be easy for anyone to implement it for their own
types and then implement Backend<TheirType> for TheirBackend. Rust doesn’t offer a way to have
sealed traits so far, so my current solution to this is to mark the trait unsafe (to scare
people and tell them not to implement the trait). However, a clear and first-class citizen language
construct for this would be highly appreciated.
That’s all for me today. If you have any question about such a design, I’d be happy to answer your questions on Reddit, as usual.
Keep the vibes!
]]>This article serves as an announcement for glsl-0.13.
I’ve been working on glsl-0.13 for a few days now since my previous blog entry about adding pest to glsl – and no, the current code is not pest-based, it’s still good old nom. Lately, I’ve been wanting to enhance my glsl-quasiquote crate to add variable interpolation. I think I will dedicate a whole article to variable interpolation in GLSL because it’s actually tricky to get done right – without duplicating a whole parser or introducing problematic code with pest (see my last article).
Since I’m not a programmer who solves problems that don’t exist, I have problems to get resolved in other crates and binary projects of mine (mostly demoscene and demoscene tooling). However, I’ve been spending days solving those problems because they’re not related to demoscene only: pretty much anyone who would like to cope with shaders might be interested.
The glsl crate provides you with:
BTreeMap for instance, because each nodes and levels are completely typed. You can picture the
AST as both a type tree and value tree instead of just a value tree.let x: Expr = glsl!{…};.
I will try to fix that later.glsl-0.13 comes with several new features and enhancements to partially fix those points that I will explain in this blog entry.
First, let’s talk about contribution. I received a contribution (both an issue and a PR, how
amazing!) from @JDemler about the #define preprocessor pragma. Those were not taken into
account so far and he provided the fix to add them! As for all other pragmas, those have to lay in
the global scope – they’re not expected to be found in function bodies, for instance.
Thanks for your contribution! :)
Then, a lot of work has been done around the glsl::syntax module. This module contains all the AST
types and some methods were added to, for instance, create functions, variable declarations,
if-else blocks, etc. Those functions were made the most general as possible and heavily depend on
From / Into, allowing for very short oneliners to create complex AST nodes. The best example
for this is the Statement::declare_var that declares a new variable as a Statement. Most of
the inner AST nodes won’t ever be needed in the public interface, so those functions hide them from
you and give you the easy way to build bigger nodes.
There is an example below that uses
Statement::declare_var. Keep on reading! :)
Not all AST nodes have helper methods for construction. The good thing is that adding a new function shouldn’t be a breaking change, so I’ll keep adding them as needed – if you have a specific need for one, feel free to open an issue, I will make the change. That might be surprising but since I haven’t witnessed lots of people using the crate yet, I don’t implement what I don’t need yet – but as soon as someone tells me they need something, either I immediately review the PR, or plan some time to make the patch myself.
Finally, the biggest feature which will be explained in further details in this blog post: AST visitors. AST visitors are the way I imagined would be the best to traverse an AST in order to mutate some nodes, all the nodes, filter, query information, etc. It’s like lenses for glsl! – but trust me: it’s way lighter! :D
AST visitors solve a problem I had when implementing a specific feature in spectra. I needed to be able to change all the references to a given identifier in all the AST at once. Either the identifier is used in an expression assigned to a new variable, returned from a function or passed as argument to a function, I needed to catch that identifier… and change its name.
That was quite of a desperate challenge without a proper solution. Imagine: I would have to write the code to change the identifier myself and… find ALL the places where identifiers might appear in any AST. This is possible, but that would drive many developers mad – especially whevener the glsl crate changes.
So I came up with a better solution. I will not drop you a technical term and have you read Wikipedia, I would dislike that. I will just explain from bottom up why it’s designed this way and why I think it’s the best solution.
The idea is that pattern-matching on an identifier might appear in several places in an AST. So whatever solution we choose, we will have to find all those spots and call a function that states:
Hey there. Here is an
Identifier. I give you a&mut Identifierso that you can change it.
An Identifier is an AST (a really simple one, but still is). So you might want to implement that
function on Identifier… but what happens when your AST is an Expr, and that expr is a variable –
that is, Expr::Var(identifier_here)? You will want to implement your function on Expr also,
then. And here you see that you need a trait, because, as said earlier, the AST is a type tree.
However, what trait? We could imagine implementing that trait only on AST types that have an
Identifier as field or variant. But the most general AST node, TranslationUnit, is a non-empty
list of ExternalDeclarations. If we want to visit that, we won’t be able to type match and pass
down our function transforming identifiers.
We see that we will need to implement that trait for all AST types so that they can pass the
function down the AST to a node that actually has an Identifier.
And since we’re doing it with Identifier, we might want to do it with any AST node. But if we
do that, we cannot pass one single function anymore…
So you need an object that will be able to treat an Identifier… or a TranslationUnit, or
anything AST. This is a bit boring to implement, but we need a trait that enables a type to visit
any AST node:
trait Visitor {
fn visit_identifier(&mut self, _: &mut Identifier);
fn visit_translation_unit(&mut self, _: &mut TranslationUnit);
// …
}
This is great, because if a type implements Visitor, it means that we can call visit_identifier
on it if we have an Identifier! When we will be implementing our traversing function, we will just
have to carry around a mutable reference to an object implementing Visitor, and call the right
function depending on the AST node / type we are at!
We also use mutable references here so that we can also mutate information as we sink into the AST. This might be very useful to know at which block depth we are at, or if we’re in a function’s body, etc.
Something important with that trait though: the current implementation (from this article) would be
very boring to implement, because it has a lot of visit_* methods. What if we’re only interested
in Identifier? We don’t want to have to implement visit_translation_unit because we don’t care.
A simple fix to that is to give all those visit_* methods a default implementation… that does
nothing.
trait Visitor {
fn visit_identifier(&mut self, _: &mut Identifier) {}
fn visit_translation_unit(&mut self, _: &mut TranslationUnit) {}
// …
}
Let’s try it!
struct ReplaceIdentifier<'a> {
replacement: &'a str
}
impl<'a> Visitor for ReplaceIdentifier<'a> {
fn visit_identifier(&mut self, ident: &mut Identifier) {
*ident = self.replacement.clone().into()
}
}
And that’s all! We now have a visitor that can be used to traverse any AST and change any
Identifier to what we have set. For instance:
let mut ast = …;
let mut visitor = ReplaceIdentifier { replacement: "foobar" }
// wait, how do we pass the visitor to the AST here?
Argh, we’re still missing something!
We need a visit function, like:
fn visit<V>(ast: &mut TranslationUnit, visitor: &mut V) where V: Visitor
So let’s write it!
fn visit<V>(ast: &mut TranslationUnit, visitor: &mut V) where V: Visitor {
for external_decl in ast {
// ah.
}
}
We cannot call visit again on ExternalDeclaration. Seems like we need another trait! :D
trait Host {
fn visit<V>(&mut Self, visitor: &mut V) where V: Visitor;
}
Here, a type that implements Host means that it can call a Visitor on itself, and might pass it
down to children AST if any. Since we’re going to implement Host for all our AST types, we will be
able to do something like this:
impl Host for TranslationUnit {
fn visit<V>(&mut Self, visitor: &mut V) where V: Visitor {
visitor.visit_translation_unit(self); // first, we have the visitor visit the AST node
// then, for all children, we pass down the visitor!
for external_decl in self {
external_decl.visit(visitor);
}
}
}
And here we go. We are able to pass down the visitor, that will be called for each node. If you have
provided an implementation for a given visit_*, it will get invoked, otherwise, the default
implementation will fire – and it does nothing.
A simple optimization can be done, here. Since you might know that you’re done at a given level
regarding your visitor, we could add a way to make a Host stop visiting and go any deeper. For
this, we introduce a simple enum:
enum Visit {
Children, // keep visiting children
Parent // stop visiting, go back to parent
}
And we change all the Visitor methods to return a Visit. That will give us the information we
need when implementing Host::visit now:
impl Host for TranslationUnit {
fn visit<V>(&mut Self, visitor: &mut V) where V: Visitor {
let visit = visitor.visit_translation_unit(self); // first, we have the visitor visit the AST node
if visit == Visit::Children {
// then, for all children, we pass down the visitor!
for external_decl in self {
external_decl.visit(visitor);
}
}
}
}
We also need to change the default implementation of the visit_* methods. My choice was to have
them return Visit::Children by default, because I think it’s a saner default – if people don’t
want to go any deeper in the AST, they will just have to dummy-implement the right method.
And we are done! The actual implementation is a bit more complex than that but is really really close to what is described in this article. I’ll give you the example from the official documentation so that can you can see how it’s really used:
use glsl::syntax::{CompoundStatement, Expr, SingleDeclaration, Statement, TypeSpecifierNonArray};
use glsl::visitor::{Host, Visit, Visitor};
use std::iter::FromIterator;
let decl0 = Statement::declare_var(
TypeSpecifierNonArray::Float,
"x",
None,
Some(Expr::from(3.14).into())
);
let decl1 = Statement::declare_var(
TypeSpecifierNonArray::Int,
"y",
None,
None
);
let decl2 = Statement::declare_var(
TypeSpecifierNonArray::Vec4,
"z",
None,
None
);
let mut compound = CompoundStatement::from_iter(vec![decl0, decl1, decl2]);
// our visitor that will count the number of variables it saw
struct Counter {
var_nb: usize
}
impl Visitor for Counter {
// we are only interested in single declaration with a name
fn visit_single_declaration(&mut self, declaration: &mut SingleDeclaration) -> Visit {
if declaration.name.is_some() {
self.var_nb += 1;
}
// do not go deeper
Visit::Parent
}
}
let mut counter = Counter { var_nb: 0 };
compound.visit(&mut counter);
assert_eq!(counter.var_nb, 3);
The current state of Visitor is great but there is a drawback: your AST has to be mutable. You
might want to only traverse it read-only but have your visitor mutated. I might then modify slightly
the Visitor trait and Host one with Host::visit_mut if I think it’s needed.
Feel free to experiment around. Next work planned for glsl – besides contributions – quasiquoting variable interpolation.
Keep the vibes!
]]>It’s been a while I haven’t written anything on my blog. A bit of refreshment doesn’t hurt much, what do you think?
As a demoscener, I attend demoparties, and there will be a very important and fun one in about a month. I’m rushing on my 3D application so that I can finish something to show up, but I’m not sure I’ll have enough spare time. That being said, I need to be able to represent smooth moves and transitions without any tearing. I had a look into a few Haskell spline libraries, but I haven’t found anything interesting – or not discontinued.
Because I do need splines, I decided to write my very own package. Meet smoothie, my BSD3 Haskell spline library.
A spline is a curve defined by several polynomials. It has several uses, like vectorial graphics, signal interpolation, animation tweening or simply plotting a spline to see how neat and smooth it looks!
Splines are defined using polynomials. Each polynomials is part of the curve and connected one-by-one. Depending on which polynomial(s) you chose, you end up with a different shape.
For instance, 1-degree polynomials are used to implement straight lines.

As you can see, we can define a few points, and interpolate in between. This is great, because we can turn a discrete set of points into lines.
Even better, we could use 3-degree polynomials or cosine functions to make each part of the spline smoother:

We still have discrete points, but in the end, we end up with a smooth set of points. Typically, imagine sampling from the spline with time for a camera movement. It helps us to build smooth moves. This is pretty important when doing animation. If you’re curious about that, I highly recommend having a look into key frames.
So I’ve been around implementing splines in Haskell the most general way as possible. However, I don’t cover – yet? – all kinds of splines. In order to explain my design choices, I need to explain a few very simple concepts first.
A spline is often defined by a set of points and polynomials. The first point has the starting sampling value. For our purpose, we’ll set that to 0:
let startSampler = 0
The sampling value is often Float, but it depends on your spline and the use
you want to make of it. It could be Int. The general rule is that it should
be orderable. If we take two sampling values s and t, we should be able
to compare s and t (that’s done through the typeclass constraint Ord in
Haskell).
So, if you have a spline and a sampling value, the idea is that sampling the
spline with startSampler gives you the first point, and sampling with
t with t > startSampler gives you another point, interpolated using points
of the spline. It could use two points, three, four or even more. It actually
depends on the polynomials you use, and the interpolating method.
In smoothie, sampling values
have types designed by s.
A spline is made of points. Those points are called control points and
smoothie uses CP s a to refer
to them, where s is the sampling type and a the carried value.
Although they’re often used to express the fact that the curve should pass through them, they don’t have to lie on the curve itself. A very common and ultra useful kind of spline is the B-spline.

With that kind of spline, the property that the curve passes through the control points doesn’t hold. It passes through the first and last ones, but the ones in between are used to shape it, a bit like magnets attract things around them.
Keep in mind that control points are very important and used to define the main aspect of the curve.
Polynomials are keys to spline interpolation. They’re used to deduce sampled points. Interpolation is a very general term and used in plenty of domains. If you’re not used to that, you should inquiry about linear interpolation and cubic interpolation, which are a very good start.
Polynomials are denoted by Polynomial s a in
smoothie, where s and a have
the same meaning than in CP s a.
smoothie has then three important types:
CP s a, the control pointsPolynomial, the polynomials used to interpolate between control pointsSpline s a, of courseThe whole package is parameterized by s and a. As said earlier, s is very
likely to require an Ord constraint. And a… Well, since we want to represent
points, let’s wonder: which points? What kind of points? Why even “points”?
That’s a good question. And this is why you may find
smoothie great: it doesn’t
actually know anything about points. It accepts any kind of values. Any?
Almost. Any values that are in an additive group.
“What the…”
I won’t go into details, I’ll just vulgarize them so that you get quickly your
feet wet. That constraint, when applied to Haskell, makes a to be
an endofunctor – i.e. Functor – and additive – i.e. Additive. It also
requires it to be a first-class value – i.e. its kind should be * -> *.
With Functor and Additive, we can do two important things:
Functor. It enables us to lift computation on the inner type.
We can for instance apply a single function inside a, like *k or /10.Additive. It enables us to add our types, like a + b.We can then make linear combinations, like ak + bq. This property is well known for vector spaces.
The fun consequence is that providing correct instances to Functor and
Additive will make your type useable with
smoothie as carried value in the
spline! You might also have to implement Num and Ord as well, though.
Creating a spline is done with the spline function, which signature is:
spline :: (Ord a, Ord s) => [(CP s a, Polynomial s a)] -> Spline s a
It takes a list of control points associated with polynomials and
outputs a spline. That requires some explainations… When you’ll be sampling
the spline,
smoothie will look for which
kind of interpolation method it has to use. This is done by the lower nearest
control point to the sampled value. Basically, a pair (cp,polynomial) defines
a new point and the interpolation method to use for the curve ahead of the
point.
Of course, the latest point’s polynomial won’t be used. You can set whatever you
want then – protip: you can even set undefined because of laziness.
Although the list will be sorted by spline, I highly recommend to pass a
sorted list, because dealing with unordered points might have no sense.
A control point is created by providing a sample value and the carried value.
For instance, using linear’s V2
type:
let cp0 = CP 0 $ V2 1 pi
That’s a control point that represents V2 1 pi when sampling is at 0. Let’s
create another:
let cp1 = CP 3.341 $ V2 0.8 10.5
Now, let’t attach a polynomial to them!
The simplest polynomial – wich is actually not a polynomial, but heh, don’t look at me that way – is the 0-degree polynomial. Yeah, a constant function. It takes the lower control point, and holds it everwhere on the curve. You could picture that as a staircase function:

You might say that’s useless; it’s actually not; it’s even pretty nice. Imagine you want to attach your camera position onto such a curve. It will make the camera jump in space, which could be desirable for flash looks!
Use the hold Polynomial to use such a behavior.
1-degree functions often describe lines. That is, linear is the Polynomial
to use to connect control points with… straight lines.
One very interesting Polynomial is cosine, that defines a cosine
interpolation, used to smooth the spline and make it nicer for moves and
transitions.
If you’re crazy, you can experiment around with linearBy, which, basically, is
a 1-degree polynomial if you pass id, but will end up in most complex shapes
if you pass another function – (s -> s). Dig in documentation on hackage for
further information.
Ok, let’s use a linear interpolation to sample our spline:
let spl = spline [(cp0,linear),(cp1,hold)]
Note: I used
holdas a final polynomial because I don’t like usingundefined.
Ok, let’s see how to sample that. smoothie exports a convenient function for sampling:
smooth :: Ord s => Spline s a -> s -> Maybe a
smooth spl s takes the sampling value s and maybe interpolate it in the
spl spline.
“Maybe? Why aren’t you sure?”
Well, that’s pretty simple. In some cases, the curve is not defined at the
sampling value you pass. Before the first point and after, basically. In those
cases, you get Nothing.
I wrote smoothie in a few hours, in a single day. You might have ideas. I want it to be spread and widely used by awesome people. Should you do graphics programming, sound programming, animation or whatever implying splines or smoothness, please provide feedback!
For people that would like to get contributing, here’s the github page and the issue tracker.
If no one comes up with, I’ll try to add some cubic interpolation methods, like hermitian splines, and one of my favorite, the famous Catmull Rom spline interpolation method.
As always, have fun hacking around, and keep doing cool stuff and sharing it!
]]>Rust has this feature called move semantics. By default, the move semantics are enforced. For instance:
fn foo_move<T>(x: T)
This function says that it expects a T and need to move it in. If you don’t want to move it, you
have to borrow it. Either immutable or mutably.
fn foo_borrow<T>(x: &T)
fn foo_borrow_mut<T>(x: &mut T)
Now, when you move a value around, it’s possible that you don’t use that value anymore. If you look
at the foo_move function, is very likely that we’ll not use that T object after the function has
returned. So Rust knows that x will go out of scope and then its memory must be invalidated and
made available to future values. This is very simple as it will happen on the stack, but it can get
a bit tricky.
Rust doesn’t have the direct concept of destructors. Or does it? Actually, it does. And it’s
called the Drop trait. If a type implements Drop, any value of this type will call a specific
function for disposal when it goes out of scope. This is akin to running a destructor.
struct Test;
impl Drop for Test {
fn drop(&mut self) {
println!("drop called!");
}
}
Whenever a value of type Test goes out of scope / must die, it will call its Drop::drop
implementation and then its memory will be invalidated.
Ok, now let’s get into my problem.
Consider the following code:
struct A;
struct B;
struct Foo {
a: A,
b: B
}
impl Foo {
fn take(self) -> (A, B) {
(self.a, self.b)
}
}
fn main() {
let foo = Foo { a: A, b: B };
let (a, b) = foo.take();
}
The implementation of Foo::take shows that we move out of Foo – two, actually. We move the
a: A field out of Foo and b: B out of Foo. Which means that we destructured Foo. The
call to Foo::take in the main function demonstrates how you would use that function.
Now consider this. A and B here are opaque type we don’t want to know the implementation. But
keep in mind they represent scarce resources – like UUID to important resources we must track in
the Foo object. Consider this new code:
struct A;
struct B;
struct Foo {
a: A,
b: B
}
impl Drop for Foo {
fn drop(&mut self) {
// something tricky with self.a and self.b
}
}
impl Foo {
fn take(self) -> (A, B) {
(self.a, self.b) // wait, that doesn’t compile anymore!
}
}
You would get the following error:
error[E0509]: cannot move out of type
Foo, which implements theDroptrait
As you can see, Rust doesn’t allow you to move out of a value which type implements Drop, and this
is quite logical. When Foo::take returns, because of self going out of scope, it must call
its Drop::drop implementation. If you have moved out of it – both a: A and b: B fields, the
Drop::drop implementation is now a complete UB. So Rust is right here and doesn’t allow you to
do this.
But imagine that we have to do this. For insance, we need to hand over both the scarce resources
a and b to another struct (in our case, a (A, B), but you could easily imagine a better type
for this).
There’s a way to, still, implement Foo::take with Foo implementing Drop. Here’s how:
impl Foo {
fn take(mut self) -> (A, B) {
let a = mem::replace(&mut self.a, unsafe { mem::uninitialized() });
let b = mem::replace(&mut self.b, unsafe { mem::uninitialized() });
mem::forget(self);
(a, b)
}
}
There’s a few new things going on here. First, the mem::replace function takes as first parameter
a mutable reference to something we want to replace by its second argument and returns the initial
value the mutable reference pointed to. Because mem::replace doesn’t deinitialize any of its
argument, I like to think of that function as a move-swap. It just moves out the first argument
and put something in the hole it has made with its second argument.
The unsafe function, mem::uninitialized, is a bit special as it bypasses Rust’s normal
memory-initialization and actually doesn’t do anything. However, there’s no specification about what
this function really does and you shouldn’t be concerned. What you just need to know is that this
function just gives you uninitialized memory.
However, if we just returned (a, b) right after the two mem::replace calls, the Drop::drop
implementation would run at the end of the function, and it would run badly, because of the now
uninitialized data you find in the self.a and self.b fields. It could still be the old values.
Or it could be garbage. We don’t care. We mustn’t read from there. Rust provide a solution to this,
and this is the mem::forget function. This function is like dropping your value… without calling
its Drop::drop implementation. It’s an escape hatch. It’s a way to invalidate a value and in our
case that function is super handy because self now contains uninitialized data!
And here we have a complete working Foo::take function.
The problem is that I used to function I wish I didn’t:
mem::uninitialized, which is,
as the docs state,
incredibly hazardous.mem::forget, which is not unsafe in the Rust idea of safety, but still has some kind of very
nasty unsafety deep in it – it’s very easy to leak resources if you don’t pay extra attention.The problem is that Rust doesn’t allow you to move out of a value which implement Drop even if you
know that you’ll forget it. Rust doesn’t currently have a primitive / function to express both
principles at the same time: forgetting and moving out of a Drop value.
This had me to come to the realization that I would like an interface like this:
fn nodrop<T, F, A>(
x: T,
f: F
) -> A
where T: Drop,
F: FnOnce(T) -> A
This function would give you the guarantee that the closure will not drop its argument but would
still leak it, temporarily giving you the power to move out of it. You will also notice that if you
want to have A = T, it’s possible: that would mean you destructure a value and re-build it back
again inside the same type, which is still sound.
However, this function would make it very easy to leak any field you don’t move out. This is
actually the same situation as with mem::forget: if you forget to mem::replace a field before
calling mem::forget, you end up in the exact same situation.
I agree it’s a very niche need, and if you’re curious about why I need this, it’s because of how
buffers are handled in luminance. The Buffer<T> type has an internal RawBuffer as field that
holds GPU handles and internal details and a PhantomData<T>. It’s possible to convert from
one type to the other but both implement Drop, so you have to implement the hack I just presented
in this blog post to make both conversions. With my nodrop function, the code would be way
easier, without needing to unfold unsafe blocks. Something like this would be possible:
pub fn to_raw(self) -> RawBuffer {
mem::nodrop(self, |buffer| buffer.raw) // combine mem::forget and mem::replace + uninitialized
}
What do you peeps think of such a function? For sure that would add possible easy leaks, that’s a drawback I find very difficult not to agree with. However, I just wanted to share my little experience of Tuesday evening.
That’s all for me. Thanks for having read me, I love you. Keep the vibes!
]]>The glsl crate enables you to parse a GLSL-formatted input (raw string or byte slice) into an AST you can manipulate in Rust.
It’s very likely you will never have to work with that AST representation directly. Instead, you might be interested in all the possible outcomes you can get from using the glsl crate:
scene.glsl, and get its AST representation in Rust.Lately, in spectra, I’ve been working on a more abstract way to write shaders in GLSL. The idea is that I need to ship the library with some GLSL. The typical way people do this is by embedding a static string into your library / application, like so:
const SOME_VERTEX_SHADER: &str =
"void main() {
// …
}";
You can also use the
include_str!standard macro to read it from a file at compile-time.
The cheddar crate provides you with an enriched version of GLSL (the Cheddar language), and the integration in spectra is pretty much seamless. The GLSL is parsed from Cheddar, transpiled to the GLSL AST and then the whole thing is passed luminance that acts as a rendering backend of spectra. Becauses some AST transformation is needed, the glsl crate is used in cheddar, and some GLSL must be added in order to provide a smoother and nicer experience within spectra.
The problem kicks in that transformation part. Since Cheddar is parsed directly into memory as an AST (via the cheddar crate), how could we transform and add things to values from cheddar?
Pretty much all of the glsl crate is public. There’s no invariant, so no need to hide anything.
It’s possible to create, for instance, a AssignmentExpr by simply calling its constructor syntax:
let assignment_expr = AssignmentExpr {
// fields here
};
This is the regular way to go, but if you look at some types, such as the
SimpleStatement, you’ll get
that it can get very noisy and verbose very quickly. So that’ll work, but that’ll also be very
incomfortable to work with.
The advantage of constructing and transforming that way is that you can destructure and pattern-match ASTs.
As a Haskeller, I’ve been very frustrated by that construction problem, thinking that there must be an easier and less noisy way to do so. That reminds me of all the EDSLs I wrote in my Haskeller life and some very, very sweet embedded languages, like SQL, C or Java, directly into Haskell. That is done via something we call quasiquoting. Quasiquoting is a general concept but in our case, applies to embedded languages.
The concept is simple: you write in a foreign language in a quasiquoter in your host language (Haskell or Rust). The compiler then recognizes the quasiquoter and executes some code at compile time. Once it’s done, it replaces the quasiquoted statement with the result of its computation.
One good example of quasiquoting is postgresql-query, a Haskell quasiquoting library for SQL. SQL queries can be built with regular Haskell code but they can also be built out of a quasiquoter, like so:
-- some code elided for simplicity here
pqQuery [sqlExp|SELECT u.id, u.name, u.age
FROM users AS u ^{cond}
ORDER BY ^{ord}|]
The [sqlExp|…|] notation uses the quasiquoter syntax: the sqlExp is the quasiquoter to use, and
everything between the two pipes are the content / input to pass the quasiquoter. The compiler just
reads that, executes some code defined in the postgresql-query library and if it runs correctly,
the resulting expression is inserted directly at the place of use as if nothing has happened.
Brillant, right? :)
Having written glsl, that seemed like a perfect – maybe even wanted and mandatory? – opportunity to me to introduce GLSL quasiquoting into the Rust environment. Maybe it’ll also bring some people onto the glsl project – I really need hands, especially because the SPIR-V transpiler doesn’t exist yet!
So, here you go. glsl-quasiquote-0.1 was released today! The crate provides you with two macros:
glsl! and glsl_str!. Both are procedural macros that requires a nightly compiler and the
proc_macro_non_items feature. They will both output a TranslationUnit, that represents a whole
shader AST.
The glsl! AST works the same way a quasiquoter from Haskell: instead of delimiting your GLSL code
with pipes, you write them inside parens or brackets, like so:
glsl!{
void main() {
}
}
The glsl_str! macro does the exact same thing but expects a literal string as input. This is due
to the fact that quasiquoting in Rust is a bit tricky – who’s looking at me like I was about to
write an RFC? – because regular macros and procedural macros eat Rust tokens as inputs, forcing
their content to be Rust-friendly syntax. Well, GLSL is not: the #version and #extension pragmas
need newlines at the end, so the following will not work:
glsl!{
#version 330 core
void main() {
}
}
Instead, you must use the glsl_str! quasiquoter:
glsl_str!{"
#version 330 core
void main() {
}
"}
It’s a bit of an unaesthetica and pretty weird syntax but we don’t really have a choice (so far).
Whenever a GLSL is incorrectly formatted, you’ll get a compiler error. For instance, the following:
glsl!{
void main() 3 {
}
}
generates the following rustc output:
error: proc macro panicked
--> tests/lib.rs:26:11
|
26 | let _ = glsl!{
| ___________^
27 | | void main() 3 {
28 | | }
29 | | };
| |___^
|
= help: message: GLSL error: Err(ParseError { kind: Many1, info: "void main ( ) 3 { }" })
error: aborting due to previous error
The crate was released today, so it needs a lot of love, attention and testing. I’m adding support for it in spectra for my experiences, so I should be updating it if I find anything wrong with it.
Feel free to use it, and have fun! I’m awaiting your feedback!
Keep the vibes!
]]>This article serves as an announcement to a new crate: proc-macro-faithful-display.
I’ve been around procedural macros in Rust for a few weeks now – in order to write my
glsl-quasiquote crate. Quickly, I discovered that most types cannot be inspected. For instance,
an Ident has no methods to get the underlying identifier (as a &str for instance). The only
way to do so is to allocate a String, for instance, via a Display use, as in:
let ident_str = format!("{}", ident);
Now, imagine you have a fully formatted stream of GLSL tokens flowing in. I’ve already blogged lately about the fact Rust has some specific rules that will prevent you to write preprocessor pragmas. The only way to authorize them was to implement a hack (explained in the blog article).
Lately, I was working on some not-so-trivial glsl transformations in another project when I hit a parse error. I was surprised, because the glsl crate contains more than 130 unit tests about parsing. So I thought “Well, okay, it’s alright, I might have forgotten a very specific case not covered in my nom implementation. I’ll just get a small reproducible sample and use it as a new integration test in glsl”. That kind of reasoning is very important to me: if you find a bug, first thing to do (if you plan to fix it) is to write a unit test that fails first. This is very important because when the test succeeds, you have solved your problem. If someone provides a contribution later and that the test breaks again, you will have just shielded the project against a test regression. Seriously, I know it’s boring but: write tests.
So I wrote a test, a very simple one (this is actually an integration test, since I’ve found the error in a project using glsl). And… the test passed. I was so surprised: “So that code breaks in my crate, but succeeds in the integration test suite?! Well, wait. That means that the parser is actually correct. The problem might come from the way the parser is used… or the input.”
So my second reaction was to have a look at where the parser was called. It was in a glsl!
procedural macro invocation – in my project, I hardcoded a vertex shader with glsl-quasiquote.
And then, I came to the realization and remembered. glsl-quasiquote, in order to parse the
input Rust token, has to use Display on them, in order to yield a String usable by the glsl
parser. The parser is okay – i.e. the integration test passes – but the input string… loses
information.
If you have taken a look at the integration test just above, you should have noticed an interesting construction:
void main() {
float x = 1. * .5;
}
This is not a semantically valid vertex shader, but it should parse. And it does parse. But if you write that vertex shader with glsl-quasiquote, what you actually write is this:
let vertex_shader = glsl!{
void main() {
float x = 1. * .5;
}
};
The Rust tokenizer will eat the tokens and the Display implementation will render the stream
by using spaces whenever it sees fit. It will not respect the initial whitespaces. It might yield
something like this:
void main ( ) { float x = 1. * . 5 ; }
Look closely: our .5 GLSL floating point constant value (it equals 0.5) has become . and 5.
rustc has transformed it into two tokens instead of a single one. This is due to the fact that
this float format is not allowed in Rust and is interpreted the same way a method call would be.
So, after realizing that, I just went for a documentation addition stating that numbers cannot be
expressed this way, and that you have to use a leading 0. I really disliked writing that
documentation exception but I didn’t have any solution to that.
A few days later, I was thinking about glsl variable interpolation and got a mind click. In order
to implement preprocessor pragmas, I used a trick with Span, that gave me positional information
of tokens. And I came to the realization that I could use the same kind of trick to implement a
function that would yield an object implementing Display by respecting the initial
layout / formatting!
Basically, a trait – FaithfulDisplay – is implemented for all flavours of Rust tokens:
All the implementations carefuly carry around the previous Span in order to pad enough spaces
and newlines before they get formatted into the Formatter. This allows to reconstruct the input
layout.
All this code is packaged in the proc-macro-faithful-display crate. You can find the documentation here. I use it – and tested against – the crate with glsl-quasiquote. It especially allowed me to remove all the preprocessor trick because now newlines are taken into account.
Honestly, I have no idea. Currently, at the time of writing, it works like a charm. If you find any
bug or weird behavior, please open an issue here.
However, proc-macro hasn’t yet been stabilized and the Span type is accessible through a
feature gate. All of this might change. However, I strongly think that:
Span type because it is really helpful (its first purpose was to provide nice error reporting).Keep the vibes!
]]>In this blog entry, I want to share something a bit different: instead of presenting my experiences with all those editors, I want to take some hindsight and analyze a bit more the technology and what it seriously means in terms of features and productivity. Obviously, I will reference other editors I have used a lot (I use IntelliJ a lot at work, and I am used to VS Code and Atom as well).
Yeah, you read that title right. I am lost. I have been a Vim / Neovim user for something like 12 years now. I am used to the ecosystem, I have installed hundreds of plugins in the past decade, tried so many things, discovered pearls that I still use today (hello context.vim), I have been using a workflow based on tmux, Neovim and my shell that has proven very powerful and yet to be fully replaced. But — and I think it is important to make that note — I did not start coding with Vim. Today, I am 29. I started coding when I was 11, using a tool called Code::Blocks. Rapidly, I switched to Emacs – by the time, Doom Emacs did not even exist, and spent several years using it. Later, when I was between 16 and 17, I switched to Vim — I guess the modal idea got me intrigued and I felt in love with it. In 2014, I was told that Vim was getting forked for the reasons we all know and I decided to switch to the fork, called Neovim.
Since then, I have been sticking to Neovim but I do not partake in those “editor wars”. “VS Code sucks”, “how do I even exit Vim?”, “Emacs is bloated!”. I have my personal opinions on the technologies (i.e. I dislike very much the front-end ecosystem / electron, even though I think the front-end idea — i.e. running code in a browser — is an interesting idea; I just think the current technology for doing so is really bad), so obviously I will not pick something such as Atom / VS Code, but I cannot say they suck. I have tried them. I see why people like them and it would be very dishonest to say those editors suck. They really do not. Same thing applies to the products by JetBrains. In the past year, I did not understand why some people would pay money for an editor. I have to use IntelliJ at work and even though I would not pay for an editor, I start to grasp why people would actually want to pay for one. Not all people have the patience and time to tweak an editor to their taste, such as with Vim / Neovim / Emacs. For those people, editors such as VS Code, IntelliJ, Atom and others are really just superior.
Just take LSP. Configuring LSP in VS Code is a click on an install button, depending on your language. That is all. You get all the good defaults and behaviors and you can already start enjoying coding with LSP. Doing the same in an editor like Neovim is a very different experience. I am not saying it is bad; it is different. If you are, like me, someone who enjoys spending time tweaking their tools, then you should not care too much. I have a colleague at work who gave me an argument that I find very interesting: he basically wants an editor he does not have to configure, so that we knows how to use it everywhere, on any machine of any colleague. That is not a point I would make, but for someone who cares about that topic, it is a solid argument in favor of tools such as IntelliJ or VS Code.
In the previous company I was working for, I worked with @gagbo, who is an active Doom Emacs contributor, and I was surprised by the editor he was using on his laptop. He mentioned Doom and on the night home, I had a look at it, and I was really blown away. Doom Emacs (Doom for short) was a revelation to me, because it is using emacs as a productivy platform more than a simple editor. @hlissner, Doom’s author, and the Doom community, have been gathering the best of emacs and vim into a single configuration distribution. The result is so interesting and so powerful that I have been doing back and forth between Doom and Neovim. More on that below.
Lately, I have been more present in the Neovim community, contributing mostly via a plugin that I have been making: hop.nvim. That plugin is a complete rewrite from scratch of EasyMotion (I have not even looked at how they do it on their side) I made by using the Neovim Lua API directly. Obviously, it provides a much better experience than EasyMotion in Neovim, simply because it uses features such as virtual text and extended marks and does not touch the buffer, allowing to benefit from Neovim-0.5 crunchy features, such as the native LSP client and treesitter parsers. I also made a bunch of PRs to Neovim core, reading a bit on the C code, etc.
All that to say that… given the experience that I have with Neovim and Doom… I have no idea what to think about “editing” anymore. Neovim is growing and lots of interesting ideas are landing / will land in the next release. I do not really know whether people will stick around Hop, but I think it has found its place — at least, I spotted a missing piece and provided it. I am not a fan of Lua but I have to admit that it attracts more people doing some awesome things. Among the things that I think are going into the right direction:
My experience with Doom Emacs really changed the way I look at software, and more specifically at “productivity tools.” See, my current workflow is based on tmux, Neovim and my shell, which is zsh (vanilla, I completely ditched Oh My Zsh as I find it useless and I uninstalled starship.rs a few weeks ago because I realized I hated the package manager version annotations on my prompt, that I actually never needed). For people wondering, we have spent so much time trying to install cool things that we forgot how powerful raw and vanilla zsh is. This is my current prompt:

At work, I need a bit more information (i.e. Kubernetes (k8s) namespace and context / cluster I am currently connected to and AWS), but as those information are not path-dependent, I put them in my tmux statusline instead.
The thing is… I just realized by using Doom Emacs that I do not need all of these anymore. Doom Emacs provides an
environment that is very similar to my tmux + neovim + shell combo. It provides even more, depending on the packages you
install. And the thing is: for each tool, emacs have more opportunities, because it is not limited by a terminal
implementation (i.e. cell-grid / line based application). So for instance, you can have a k8s / AWS / docker client
inside Emacs that is likely to be much much better than the one you will use in CLI (I honestly dislike kubectl a lot,
it has so many switches and grepping through them by piping with rg or using some switches such as -l feels weird).
You can easily see a buffer containing the list of pods / containers / whatever, that you should be able to work with
the tool you already use (i.e. Ivy for instance), interact dynamically with (instead of having to type one line after
another to make effects). It is even more true when you consider the actual shell: you do not need zsh anymore when
using Emacs, because it has its eshell implementation that is both super weird and super nice. You want to script
something for your shell? Just write ELisp. You do not have to use that horrible bash script language, and you will
benefit from the whole ELisp ecosystem. Completion for commands in eshell is basically the same as completing a regular
buffer, which is weird at first but actually makes a lot of sense. And you have dozens of other examples. Org-mode is
one of the best plugin of Emacs, allowing to take notes, make wikis, handle tasks and calendars, in a way that feels
very seamless with the rest of the Emacs ecosystem. I know some people will tell me to use another tool (I do, my
current tool for that is toodoux, a tool I made, similar to task-warrior). And of course, magit, which is by far
my favorite Emacs plugin. As others say it, it is hard to explain why magit is so much better than the git CLI or
VS Code’s integration or basically any other editor. It is just made very well, with pretty much anything actionable
from the UI, with amazing workflows and even actions you do not have easily with the CLI version.
Today, when I look at Doom, at Neovim, at what people are trying to do in Lua with Neovim, I feel like I need to stop trying to implement / follow what others are doing and think a bit about whether we are doing the right thing. Neovim, currently, is going towards a very exciting direction with all the effort regarding Lua, LSP, treesitter and community plugins. But I think the problem is bigger than Neovim. And I think I know that because of how I can use emacs with Doom. I think I come back to Doom so often because they understand well what it means to create a productivity platform more than an editor. It does not mean the editor is any less good, au contraire. In Doom, there is a default package that gets installed if you ask for the project module. That package is projectile, a wonderful package that provides with you project management from within emacs. If you use emacs in daemon mode, it means that you can get the same workflow as with tmux, Neovim and a shell, but with a native implementation of this project / workspace workflow, in a much much more powerful way (because it will obviously plug in things like Ivy, allowing you to do fuzzy searching of projects, etc.). The overall experience by having a consistent platform is something I had completely ignored for years and now that Doom hit me, it hit me hard. I do not know any other platform like emacs, besides composable environments such as tmux + Neovim + shell. Yes, you have VS Code / Atom / IntelliJ / etc. that will have tons of plugins to get you terminals and other kinds of integrations, but for having used them, it is clearly not as powerful as what Doom does — and it is most of the time based on electron; big no. To sum up, Doom is a bit my VS Code: it provides this amazing productivity platform, but it is fully native, modal-centric and highly composable / customizable.
Composability is really important to me, but I have to face reality: there is no way to make a workflow such as tmux + Neovim as snappy and fast as projectile. tmux basically reimplements a terminal inside a terminal, which has a lot of implications (copy-pasting with the mouse in tmux is a challenge even Chuck Norris does not want to take). And once you turn the computer off, tmux loses everything. You have plugins, such as tmux-resurrect, that will allow to save and restore the tmux layout (sessions / windows / panes), but you will obviously lose the contents of each panes. With projectile, because it is a native emacs thing, you will not lose anything.
This whole idea has been obsessing me for months. The concept of a “productivity platform.” People laugh at emacs as being bloated but… it basically implements their shell, tmux and editor in a more elegant way — and more. Why would one laugh at that? Especially when it allows more interesting kinds of interaction.
So I have been wondering: what is a productivity platform, today, in 2021? Is it this workflow I have been using since I am 16, with tmux and Vim / Neovim? A friend on IRC told me lately that if I have been using this workflow (as others do) for so long… it has proven being good so why would I want to replace it? Even though this is a valid point, I do think this is missing the point: ask some workers to dig a hole for planting trees, they will start digging the ground with their bare hands. Later, give them a shovel and they will accept it as a better tool and will enjoy it. But does it mean it is the best tool for planting trees? What if a better tool existed? This is basically the reason why I never accept something as an ultimate solution: we can always enhance our tools. And having seen what Doom Emacs do, I do think that I have found new ideas. Not necessarily a better tool, but at least, I think Neovim needs to evolve.
When I think about Neovim, I try to focus on what makes it this tool I love: modal centric, snappy / performance, integrates well with tmux, and that is pretty much all. The integration part with tmux is needed because I have to use the TUI, which is also something that I start not liking anymore. I know since Doom that we can get rid of TUIs and make great GUIs. So what I think I want to go to with Neovim from now on is this:
Something around one year ago, I chatted with core contributors, telling them that something that puzzles me a bit with
the current Neovim situation is that there is no unified / consistent UI experience. Most plugins will write their own
UI thing, obviously often based on TUI hacks, such as using characters as ─, ╮, ├, etc. The current situation with
plenary.nvim is another interesting topic, because I think it is not the
answer to my problem. Yes, it will provide an abstraction for people needing to create bordered windows / pop-ups but…
the GUI really does not care about that. Technically, Neovim already makes the distinction between “buffers” and
“pop-ups” and “floats”, so GUIs are free to render them the way they want, but… what if developers start using APIs,
such as plenary, where they pass border arguments? How does that make sense? If you are writing a plugin that needs to
list things and requires a fuzzy interaction with the user, you should not depend on something that expects anything
from the UI implementators. And for this, I think Neovim is lacking abstractions. Obviously, we can discuss that kind of
problems for other topics:
I guess some of those items are already addressed, but I have yet to see a Neovim GUI going fully “native without TUI aspects.” Maybe it already exists and I need to search again. I want to invest time in things like goneovim and some other implementations, and I really do want to make my own (maybe with Gtk or even using my own 2D/3D graphics Rust stack).
CLI stands for Command Line Interface; TUI for Terminal User Interface and GUI for Graphics User Interface.
Among all the things that Doom changed in my mind, there is this concept of TUI. I have been a very big user (power user?) of the CLI. Pretty much anything that I do on a computer is via the CLI. For more advanced usage, I tend to use a TUI. I only use GUIs if I have to, such as firefox, blender or a video game. However, guess what: Doom hit hard here too, because I am not using the TUI version: I am using the GUI one. And do not get it twisted, I am not using my mouse, and this is a very important and interesting point about CLI / TUI / GUI.
When talking about this CLI / TUI vs. GUI thing around me, people tend to agree, weirdly, on some misconceptions:
These misconceptions are interesting because, as misconceptions, they are not real. You can find some terrible CLIs
(i.e. npm is such a bad CLI — I had a big issue at work a few years ago when running not on purpose a command such
as npm i --user, thinking it would install it in user space while it just completely ignored the unknown --user
flag and installed the package globally), and you can find terrible GUIs too. However, and this is where I want to
focus, GUIs are not necessarily mouse-focused, and since we can implement nice UX in TUI… we should be able to do the
same in GUIs, right?
Aaaaaaaand… enter Doom, again. I am using the GUI version of emacs — on Archlinux, I am using this patched version which is a basically gccemacs, an emacs that recompiles all the Lisp as C! It obviously can accept mouse inputs so that you can click on things if you want to but… the most important still applies: you can use it exactly as you would use it in a terminal. However, because it is a GUI, you get access to much more possibilities:
/|\* ASCII art mimics we see in all CLIs / TUIs.Today, I do think that the right direction is to allow developers to build platforms around the concept of “graphics
toolkits”, and not “terminal applications.” Even for CLIs. Think about git branch. My current git branch workflow, in
CLI, does not use git branch only: I have an alias for each Git commands that require a branch. For instance, if I want
to switch to another branch, this is what my gs alias looks like when invoked:

This is basically a fuzzy-finder version of git switch, using fzf – if you want that script, have a look at
these ones. The same feature is doable with Doom / Neovim
by using the right plugin, and I have to admit that I prefer doing that in magit as it feels more “well integrated.” Of
course, you will always want to have a way to compose applications, and for that, CLIs are great. Something such as
kubectl get pods -l service=my-service | rg whatever. However, I am trying to challenge my own conception of
“program composability” because I do think we can implement this list k8s pods -> filter them inside an UI such as the
ones in Doom Emacs, without having to write a specific tool. And we can actually do it in CLI / TUI as I just showed
with my gs alias, so… why not providing that kind of power natively on the platform?
And now one of the big issue I have with pretty much any editor today (but more with Neovim as I am actively contributing to it): if you want to configure your editor, you have to code your configuration and I highly dislike that. It is possible, indeed, to reduce the quantity of code to write, but you will eventually have to get your hands dirty and write some Lua (eeew). And I have a big issue with this.
See, configuration should be stupid and limiting. It should be key = value, period. If you need a more structured way
to configure things, just add sections / objects like what you can do with TOML or JSON, but that is all. Remember that
a configuration file might be read by a human, and that you must do everything possible to make it super clear what
the configuration state is. Configuring should be:
There is a fundamental difference between having the power to do something and explicitly refusing that power and lacking that power. I think that configuration can be done via code, but it does not mean it should. The reason for that is mainly for cognitive complexity reasons. If you are the author of your application, you will very likely know how to script it. You will know all the quirks and all the little things to know that you will, obviously, have well documented (right?!). Your users will not have this kind of knowledge. I think it is wrong to use Lua to configure a software as it will force people to get into the documentation of the Lua API, while all they should have is a list of configuration options they can set, what setting does what and what are the possible values, and that is all. Providing more power leads to inconsistencies, broken setup and something that is pretty hard to fix without proper semantic versioning (and an actual package manager enforcing it): a slowly rotting configuration.
Rotting configuration happens when you start piling up lots of “configuration code”, using different APIs, that will eventually get deprecated or not even used anymore. If you are lucky, the application will just tell you this and that API calls are deprecated and you should switch to something else. If you are not, you will get some random crashes and failures in your applications and you will have to debug code. For configuration. And this happens a lot with Neovim.
See, blurring the line between configuration and scripting is easy, especially if you use the same language for both. Scripting is enhancing the power of an application, such as an editor or a video game, by providing new behaviors. Those behaviors (should them be native to the applications or new ones provided by plugins) are configured. If you start having to script the configuration side, you lose the distinction between both.
At work, we are using k8s a lot. In order to configure our services, we have to deploy configuration massively everywhere, and we need to generate that configuration. But see, there is a difference between “the configuration” and “generating the configuration.” The “generate part” is basically a template language. This is code, because it relies on the datacenter / cluster / environment / feature flags / etc. we want to deploy in / with. However, we do not configure the software in the template code. We pass fragments of configuration to the template engine, which generates the final configuration. And those fragments are basically exactly what I described above: key-value objects, sometimes nested to implement more complex configuration setup, but nothing else. It is basically YAML. If you do not need the complexity of the datacenter, cluster or environment, you can just run your application with a configuration file that has no code in it, just a bunch of key-value pairs.
The way Neovim treats configuration right now is via a convention, well-accepted among the plugin developers (and
since I am now officially a Neovim plugin developer with hop.nvim, I obviously follow it as well). That convention is
the setup function plugins expose to pass configuration (via an object often called opts for options). So people
have to know whether that convention is used, they have to call that function (i.e. it is a regular Lua function call)
and they have to dig in the Lua API. What I think I want to try (I think I am going to work on a plugin for that) is a
way to have a central “configuration store” so that users do not have to go through this setup thing anymore — which,
for the record, requires you to do something like require'the-plugin'.setup { … }, for every plugin you want to use.
The way I see things would be more like this:
-- In your init.lua, for instance
require'config-store'.setup {
telescope = {
-- options used by telescope
},
hop = {
-- options used by hop
},
neogit = {
-- options used by neogit
},
-- etc. etc.
}
require'telescope'
require'hop'
require'neogit'
Then, each plugin, in their init.lua, could do something like this:
-- Assuming this is the plugin for hop
local config = require'config-store'.hop or require'hop.config'.defaults
This way of doing seems much more natural to me, as it limits the amount of “coding” people have to do to configure
their packages, and it is not really different for package maintainers. Instead of exposing a setup function, they can
just get the configuration via this config-store thing. I think I will try to experiment with this idea. Obviously, it
requires plugins to adopt this config-store thing, and I do think that if people think it is an interesting idea, this
should be a vanilla Lua API and not a random plugin, so that we know this config-store object is there, whatever
plugins the user have installed. As discussed above with how we do that at work, I do not want to prevent people from
coding / scripting their editors. But I think a clear distinction between scripting and configuring is important.
So those were my (lost) thoughts about the current situation. I might do a follow-up later in the year to see how things have evolved to me. Today, I stick around Neovim in tmux, using various scripts around fzf to make my shell a bit more interesting to use. I have to admit that I often open emacs to do three / four commits. The experience is just consistent. Navigating in projectile, Ivy and magit is unbeaten to me. The simple interactions with dired (to navigate directories a bit like with a file browser) and edit their contents is something I truly seek everywhere else, even in my shell. I recently did a quick benchmarked of Avy, the emacs implementation of EasyMotion, versus hop.nvim, and Hop is really much faster, so I do not really know what to think (I guess I should be proud of what I made 🙂). I think that Doom currently has this level of maturity that Neovim will take years to achieve, not because of talent, but because of the fact Neovim is an editor and has not been designed as being a productivity platform, while emacs was.
Keep the vibes!
]]>I have been in that situation where I struggle with plugins breaking every time I update, and not only in Neovim. The problem can occur pretty much anywhere. The main reason to this is simple: breaking changes happen, and must happen. Plugins maintainers might decide at some point that something must be removed, or changed. A configuration option you were using disappears or its name changes. That happens and it’s part of the how software works.
However, there is a difference between breaking changes in plugins and breaking changes in i.e.
libraries / binaries we write in languages supporting package managers, such as Rust and cargo,
Haskell and cabal, Python and poetry, etc. etc. Actually, there are two main differences,
depending on the perspective you take to look at the problem:
About the last point, when I work on a given software project, I specify the dependencies of my project, and I specify the versions of those dependencies. Using SemVer, I’m sure that if the those dependencies are correctly written, I should be able to release my software (lib or bin) without any issues on my users. There are exceptions, but those are rare and most of the time related to human errors when implementing SemVer incorrectly.
When you write a plugin, you will most likely write it in a language and ecosystem that doesn’t even support versioning. Which brings me to the main point of this article: how do you prevent breaking your users?
Neovim plugins are often implemented in package managers like
packer via Git URLs. Most of the time, foo/bar will be installed as the bar plugin, part of
the org / user foo on GitHub. You can also install local plugins but that’s off topic here. When
you provide the description of the plugin to install, you will most of the time provide nothing more.
This will default to install and synchronize the plugin using the master branch (or whatever your
local git says, like main). As a Software Engineer, to me, this is a really bad habit we all
have, and I’ll explain why.
When you depend on the latest version of something, you are basically signing a contract between
your local system and the remote system to keep your local version in sync with remote. If the
remote gets updated, you get the changes the next time you update. The problem with this is that
master contains zero information about whether an update is a breaking change or not. It’s the
same reason why defaulting API calls to your API service to the latest version of the API is wrong
and that you should default them to v1: because it will never change, so people have to opt-in
for breaking changes instead, which is a much saner way of dealing with breaking changes.
So I’ve been thinking about this problem for quite a while now. In ecosystem like Rust, crates have
to implement SemVer, so that cargo, the package manager and build system, knows how to resolve
the dependency graph given your version requirements. But we don’t have that with Neovim plugins… or
do we?
In packer (and probably others), we can provide additional information to download (and update)
plugins. For instance, packer supports the branch = …, tag = … and commit = … configuration
options. So this gave me an idea. And this idea is something I would like to turn into a proposal
for the Neovim community. The idea is to use both branches and tags to encode SemVer information
and allow users to specify plugins a bit like you would specify dependencies in tools like cargo.
In order to do that, plugin authors must accept and implement SemVer. The current way I see things
is actually pretty simple:
master, main, etc. can be used as a nightly channel. Users who don’t want to deal with this
SemVer idea can just ignore the whole thing and still use their plugins the way they’ve always
had.vM.m.p, represents a full, immutable version that cannot be changed. This
would allow a user to depend on vM.m.p and be sure that the plugin would be pinned to that
version. Here, M is the major version, m is the minor version and p is the patch version.
Every time the plugin is modified in a way that doesn’t show on the interface (no breaking change
and no non-breaking change, only internal patches), the p version is incremented.vM.m, with M the major and m the minor version. The idea is that every
time a plugin gets a new non-breaking change, the m version is incremented. That would allow
users to depend on vM.m to be sticky to that minor version, but still receive patches updates.
More on that below.vM, with M the major version. Every time the plugin gets a new
breaking change, M is incremented. This would probably be the most interesting options for
users, as it would allow them to benefit from new features without having their plugin break on an
update.In order to implement this scheme using git branches and git tags, there is one trick to implement. One a plugin author decide to make a new release, they have to do several things:
v1.2.3, they will either create v1.2.4, v1.3.0
or v2.0.0, depending on the kind of change.v1.2.4.v1.2 so that it now points to the same commit as v1.2.4.v1 so that it now points to the same commit as v1.2.4.v1.3.0.v1.3 and make it point to the same commit as v1.3.0.v1 so that it now points to the same commit as v1.3.0.v2.0.0.v2.0 and make it point to the same commit as v2.0.0.v2 and make it point to the same commit as v2.0.0.With this approach, a user can now depend on a version and be sure not to get breaking-changes. An example for my config for hop.nvim:
use {
'phaazon/hop.nvim',
branch = "v1",
config = function()
require'hop'.setup {
keys = 'etovxqpdygéèfblzhckisuran',
}
end
}
I’m currently writing a (small) plugin, branching on package managers such as packer, to be able,
from a user perspective, to get a listing of packages that can be upgraded. For instance, if you
depend on telescope.nvim-0.4, if telescope.nvim-0.4.9 is released, packer should pick it up
because you would have branch = v0.4 in your config (and that branch would be updated to point to
git tag v0.4.9). However, if telescope-nvim-0.5 is released, that tool will provide you with a
hint that a new version is available and that you need to manually select it, because it might break
your configuration.
The obvious advantage (and incentive) here is that if plugin authors accept to implement something
like this, plugin stability will be an old bitter memory. People who don’t care and want to live on
the bleeding edge can completely ignore this proposal and still use the master / main branch
(not providing the branch unfortunately often defaults to the master branch). The other incentive
here is that plugins (like the one I’m writing for the tooling) can now do more things with git tags
and branches to display more information, like the number of releases, git tag annotations to show
proper release notes inside Neovim, etc.
The drawbacks are not negligible: implementing such a SemVer proposal using git tags and branches
is going to require a bit more work, and the tooling is clearly lacking. This is very similar to how
dynamic relocatable objects (.so) are handled on most Linux systems: the actual .so file is
versioned somewhere on your file system, like /usr/lib/foo.1.2.3.so, and applications / other
libraries can depend on major and minor versions by using symlinks: /usr/lib/foo.1.so points to
/usr/lib/foo.1.2.so, which in turns points to /usr/lib/foo1.2.3.so. git doesn’t support sticky
branches (i.e. you can’t ask to make a branch reference another one, it has to reference a commit,
which is always immutable), so it means that updating a version requires you to update the whole
hierarchy of branches, which is a bit annoying. A tool (like a shell tool in the plugin I’m writing)
could probably help with that.
So what do you think? Worth it? I think that in terms of stability, it is a missing piece of pretty much any editor supporting dynamic plugins. The « ideal » situation would be to support semantic versioning directly at the package level (which is not packer, but directly how Neovim represents package), and the actual encoding of the package versions would probably be hard to implement using only a centralized system like GitHub as there is no index (besides git tags and branches).
]]>git, a coding
editor, a terminal to run various commands, like compiling, testing, etc. You might also know a lot of people (if you
are not one already) using modern editors and IDEs, such as VS Code, IntelliJ, etc.
I belong to the category of people who enjoy composing tools. That is, I don’t use an IDE: I make my own IDE by combining several tools I like. In the end, we all use “IDEs” (we all need some kind of development environment). For the sake of generality, I will call those environments “DE” for development environments.
The tools I use are, basically:
The more that I think about all the things I use and all the things people enjoying modern environments use, the more I feel I try to “force” a workflow that used to be the right one a couple of years ago, but not necessarily today. I’m the kind of person who constantly put things into perspective and try to balance my decisions regarding the time I have, the budget (for work) and the knowledge. A couple of months / years ago, I spent a lot of time on emacs and realized that I needed to review my perspective, because even though emacs is super old, it’s fascinating how bleeding edge it still feels (and I can’t even imagine how bleeding edge it was twenty years ago!).
I want to use this blog article to ask questions and not necessarily answer them, but discuss about them. The questions are about “how we should be developing in the modern era” and is not about “is emacs better than neovim” or that kind of useless debates.
The first question I’m wondering is that whether using several tools and composing them is actually good in 2022. For instance, the best example I have in mind is Kubernetes and fzf. At work, I have a workflow where I use Kubernetes and filter/select its output via fzf to run other commands. I do the same with git, btw. I have some scripts where I can do something like:
gr -s # equivalent to git branch | fzf | git rebase origin --auto-stash
gs # equivalent to git branch | fzf | git switch
gm # equivalent to git branch | fzf | git merge
kubectx # switch via FZF k8s clusters (kubectl command)
kubens # same but with k8s namespaces
# etc. etc.
All of that seems to be pretty good. We can leverage the power of small tools such as git, kubectl and fzf and
compose them. Shell pipes allow to connect applications together, reading and writing from and to stdin and stdout.
The “main platform”, the “DE” is then the terminal and the shell, and building a DE requires two main ingredients:
fzf is excellent at building a list of items and let the
user pick one of them, for instance. kubectl allows to operate Kubernetes resources one operation by one operation.However, I think that even though all of that seems pretty great, it suffers from a major drawback: everything is text and everything is static. See, all of the programs I mentioned here are CLI, not TUI (Text User Interface). That is, the UX is the following:
That way of working has been a default in the industry for decades. If the program printed more than one terminal page,
you will have to scroll to find your information back up. Once you find the information you need, how do you pick that
information? If you are using a terminal that doesn’t support hinting, you will have to use your mouse (which completely
defeats the purpose of using CLI / TUI applications to me) or the keyboard-base navigation system of your terminal to
make selections, which is, let’s be honest, really poor in most terminal. If, like me, you use a terminal supporting
hinting (i.e. being able to enter a mode that will allow you to visually select parts of the screen by simply typing a
few characters — similar to my hop.nvim plugin, but for the terminal), you will be able to copy parts of the screen,
but that’s all. If a program returned a JSON array, it will cost you a lot of keystrokes to reduce the list and, for
instance, use an item in the array as input of another curl request, for instance.
The nushell has a very interesting concept to solve that kind of problem. Programs write lines of text as output, but
they can also write cells, which is a nushell encoding that allows it to “speak the same language.” It comes with
some built-in programs such as open that will be able to recognize a lot of formats (JSON, XML, YAML, TOML, etc.) and
convert them into cells, allowing to display them in the terminal in a very convenient and “smart” way. However, it
doesn’t change the next problem: composition iteration.
I think pretty much every programmer out there can relate to the following: you will oftentimes run a command, see its result, then run the command again, piping it to a filter command or sort or field extractor like [jq]. If the output is too long, it’s likely you will output into a file and will work on that file (which is basically a cached version of your command output ;) ).
Iterating on a “session” is a pretty bad experience in a terminal to me. To my knowledge, it would be quite easy to fix
by simply allowing to automatically cache the result of all commands, and get access to it via a shell variable (such as
what we have with $? for the last command status, for instance). Some hacks exist to recompute the last command
(i.e. $(!!)) but it will still re-run the last command.
I thought nushell supported that, because the variable support and Nu language are pretty excellent, but to my surprise, that shell doesn’t have that. Maybe it’s a technical challenge, but I really doubt it. We work with computers with at least 8 GB of RAM these days and most of us have between 16 and 32 GB of RAM. I don’t think it would be that hard to support.
But even if we had that caching… it would still require us to run a command, look at a static text, run a filter command on it, look at the new output that would be appended to the screen… it doesn’t seem super nice in terms of UX to me. And imagine having to change the second or third command in the pipe list while you already have six pipes. Duh. The problem, to me, is that the terminal application scope is being overloaded. The CLI is great when we want to run a command and expect a report. As soon as we need to work with data, I think the CLI is already lacking features.
And now, let’s talk about TUIs. As a lot of people, I’m using TUIs, so don’t get what I’m about to say twisted. I use neovim as my daily editor driver, and so far, I haven’t really found a way out and use something else. However, it’s not because nothing better exists that what we use is perfect (far from that).
I have always despised TUIs. I think the idea, which dates from a while, was a fun and interesting idea back then, when graphical elements and graphics kits were not mature and not a thing yet. But today, we know we can have graphical elements that align at the pixel level. We know that we can have real vector images in applications. We can use the GPU to draw arbitrary shapes and some terminals (text-based only!) use the GPU to render glyphs (text), and then people use TUIs that will use the glyphs to mimic graphical elements! That hitches me!
I dislike TUIs because they are sub-optimal versions of GUIs. And here, I think a lot of people make a confusion with UX and UI. A GUI can be beautiful, rich of graphical elements, and have a terrible UX (it’s the case most of the time). People writing GUIs tend to focus on mouse-driven interactions… which doesn’t have to be. So people tend to associate GUIs with mouse-only workflows, while a GUI can completely do everything a TUI does… but it can also do much better.
A couple of arguments against TUIs:
So let’s do a quick recap of what’s wrong with today terminals:
Those are the three main pain points I feel about using a terminal. Now, you might already have used something like a monitor application (I work at DataDog, so I’m used to them) or used a web browser or native GUI and see that you can have animations, drop-down lists that actually look good, etc. Putting all that in contrast with terminals makes me a bit sad, especially in 2022.
My latest blog articles are centered around that topic:
So it’s been an important topic to me lately. I have been thinking about productivity platforms for a while now, and the conclusion I came to is as follows.
Instead of switching to an IDE like IntelliJ or VS Code, I still think we need composition, but not in the form
of pipe shells. I think the CLI has to evolve. I have started a project on my spare-time to try and change how programs
would work with their environment. See, a shell is basically just a way to call the exec syscall (execve on Linux
for instance) and set its arguments, environment and standard input / output (plus all the candies you can get from a
shell, such as scripting languages, etc.).
The interpretation of the inputs and outputs of a program is actually completely left to… the running process, which is most of the time the shell for CLI applications. This is why nushell is such an interesting new shell: it can interpret more than just lines of text. And I think we need to move in a direction similar to this.
I think we should see more alternative forms of “terminals”. We could even imagine a program that doesn’t have the
concept of “shell” nor “terminal” anymore. It’s easy to imagine having an application that can simply call execve and
do whatever it wants the environment, inputs and outputs. It could have some primitives implemented. For instance,
imagine that a program could be run in your “new terminal” and output its results in a tree view, while other programs
are also running, displaying their own things. You could have a program run without exiting, updating the content of the
tree view or even accepting filters, sorts, field extractor or whatever see fit.
Today, text editors are overloaded. Heck, even small editors such as neovim now have a community developing plugins that have nothing to do with editing. For instance, fuzzy searching files or Git management. When you think about it, if the platform (i.e. terminal) had better primitives, it should be possible to compose programs in a way that doesn’t require people to rewrite the same kind of logic (i.e. fuzzy finders) in all the programs they use. fzf is a good example of that, but it’s limited to the CLI (you can still use it in TUI but to me it’s a bit hacky). Terminals should have a primitive to, for instance, run a program in overlay and pass the output to another, running program. That would allow people to write “pure generic fuzzy finders” once and for all, and let people use the editors they like.
Today, all that logic doesn’t really exist outside of individual and scoped efforts. Yes, you can probably find programs supporting that, but they will certainly not run in a terminal and will have their own protocols.
Composing applications with pipes is easy to wrap your fingers around, but it gets limited and limiting as soon as you use something like a GUI. Imagine that programs could speak a common language, based on, for instance, local sockets, to exchange messages in a way that whatever the kind of programs you write, you can still compose them. Composing two GUIs between each other should be something that is widely supported, and standardized.
Something else with piping applications: it’s limited to how we use CLI applications, which is via the command line. Running programs in the command line takes the form of:
program <arg1> <arg2> … | another_program <arg1> <arg2>
This is highly summarized, but you get the idea. CLI programs don’t provide a great discoverability experience, and it’s
harder to iterate on them. For instance, in a GUI, if you filter a list with a predicate, it’s super easy to
“experiment around” by changing the filter name. In a CLI session with piped programs, you are likely required to use
your up arrow on your keyboard (or whatever shortcut) to invoke the last command again, and change the arguments in the
middle of the line. While doing this, you will still have to run all intermediary commands, which are going to perform
side-effects, etc. etc. And if you want to be smarter and use the > or >> operator to cache the results, then you
will still have to remember the name of the file you used, and cat or tee that file again. It gets time to get used
to using that, and I truly think that it’s not really worth it, because today I’m 30 and I’ve been developing since I’m
11, and I still don’t really enjoy that kind of process.
The essence of an application, today, is actually platform-dependent. Heck, it’s even environment dependent. At work, an “application” can be as simple as a small Python script or be a Kubernetes cluster composed of several deployments, stateful sets and DNS rules. However, when we think about applications we use on a daily basis (what people would call “heavy” or “native” applications), they are pretty similar to the following:
stdin, arguments),
they output something (stdout) and they can have side-effects (calling another program, changing shared memory,
doing stuff on your file system, various kinds of I/O, etc.).stdin nor output to stdout in a one-shot way. Instead,
it’s like an event loop that reads from stdin and a render loop that writes to stdout.stdin and stdout in
terms of UX; GUIs can). A GUI application can decide to use OpenGL, Vulkan or something higher such as Gtk,
Qt, or a Web kit.I think that we should change that. Remember that running an application is just a matter of calling execve (or
similar on non-Linux), which basically requires a path to the application, the list of arguments (the argv) and the
environment (envp). Setting the environment is typically done by the shell (and some variables come from the terminal
too), but it could be done completely differently. On the same idea, the argv is filled by the CLI arguments you
provide, but they could be computed and filled in a completely different way. And finally, the interpretation of
stdout (mostly done by the terminal) could be interpreted in a completely different way as well.
Imagine that we could write to stdout the port / socket on which we would like to open a communication with a new,
modern protocol, that would allow us to manipulate a new kind of “terminal.” Instead of using a regular text-based
terminal, it would be a piece of software allowing to manipulate pop-ups, text buffers, image / vector image buffers,
notification queues, etc. etc.
Obviously, I’m not inventing all that. If you know about emacs, you know that that’s basically what they do, but
instead of using stdin, stdout or execve, they just evaluate Elisp. However, what I think could be very
interesting would be to allow native applications to do the same thing, with a native platform that would ship without
any real applications (so unlike emacs), and let applications implement their own workflow.
That’s basically what I’ve been working on and experimenting lately. A platform that provides buffers, tree views,
notifications, pop-pups, overlays, etc. to allow any kind of applications that can read and write to sockets to use
those features as if they were native, and platform-independent, in the same way that you use stdout without even
thinking how it will be displayed in the terminal.
Leveraging such a platform would solve most of the problems I mentioned above, because:
git application) that would open an overlay, for instance. That
application wouldn’t even know about the code editor: you would make that connection via the platform.Eh, of course, I know that such a platform protocol will probably never appear unless I make it, very opinionated and subject to never be adopted by anyone else but me (or a small group of enthusiasts about that kind of ideas). Maybe this blog article would have given you some ideas or hindsight about your own tools. Or maybe someone knows such a platform (that I don’t) and will make me realize it already exists, or is a work in progress. Maybe, after all, it will just launch a discussion about what developing means in 2022. Because to me, using tools that used to be efficient and proved to be great ones a couple of years ago, doesn’t mean they are still a thing today. And I really mean it, because I’m currently writing this very article in neovim, in a kitty tab among dozens, and I just wish I had the DE I just described above.
Keep the vibe!
]]>
Because Kakoune doesn’t have a plugin interface, you are limited to what is called kakspeak, which is basically what
you can type in the command line (the : line). Contrary to other editors like Vim, everything you type in that :
line can be laid out in a file (a .kak) and be interpreted directly. Another cool aspect about that is that you can
just select some part of a .kak file, and simply type :^r.<cr> – in Kakoune, the . is the selection register. That
will execute the Kakoune command.
This property is really good, but still, you are limited in what you can do. Among the command you will be running:
declare-user-mode, to declare your own mode (yes, like insert mode, normal mode, etc.). Yes it’s a default / core
feature and yes it’s excellent.set-face to declare / update highlighting groups.execute-keys and evaluate-commands to execute keys as if the user typed them, in sandboxed / isolated
environments. This is incredibly powerful, as you can run some commands that creates, updates registers, move the
cursor around, etc. but since it’s sandboxed, as soon as the command is done, you get back to the state you were in
before issuing the commands.There is nothing to write plugins… except one thing.
Kakoune has this concept of expanding strings. It’s the same feature as in any other editor: some strings can contain
special keywords and identifiers to replace their content with what they hold. For instance, in your shell, it’s very
likely that this string will contain your username: "$USER", or this will be the current year "$(date +%Y)".
However, this will remain verbatim: '$(date +%Y)'. The reason is that, in a shell, " expands while ' doesn’t.
Kakoune has the same mechanism, but the syntax is different, and Kakoune has different source of expansions. For
instance, %opt{foo} will be replaced why the foo option, that can be set with set-option. %val{timestamp}
contains the current timestamp of the buffer, etc. etc.
There is one interesting expansion that Kakoune has: %sh{}. This is shell expansion. It will execute its content
inside a thin shell. For instance, try open Kakoune and enter in the command line, something like :echo %sh{date +%Y}.
You can see where we are going here. We can use that to run arbitrary shell commands.
Kakoune is monothreaded, so when you run a command in a shell, it blocks until the shell command finishes. On its own, it’s not that bad. It forces us to call short-living shell commands.
This mechanism allows us to run external programs, but it doesn’t tell us how we can call back Kakoune.
Kakoune is monothreaded, but it’s concurrent. It listens on a UNIX socket Kakoune commands that any external programs
can send, via the kak -p interface.
I initially tried to send content to the UNIX socket programmatically, but it wasn’t planned for that, and hence hit issues while doing so. It hurts to say that but you have to spawn a programm running
kak -p; forget about writing directly into the UNIX socket for now.
So, with this mechanism, we can:
%sh{} expansion to talk to, for instance, a server, quickly accepting our request
and treating asynchronously.kak -p program, using the UNIX
socket.The cool thing is that Kakoune follows a server/client architecture and has a session identifier (you must pass it
to kak -p). You can retrieve that value with %val{session} — and inside %sh{} expansion, it’s available as an
environment variable $kak_session.
And here you have it. The formula I’ve been using successfully to add tree-sitter support to Kakoune. Whenever we
need to highlight the buffer again, simply craft a small request to send to the kak-tree-sitter local server and
immediately get control back (so that the UI doesn’t freeze). Then, at some point, the highlight request is computed and
arrives via kak -p in Kakoune.
But you might have a question… how do I deal with the buffer content? Indeed, the only thing we can do with with %sh{}
is starting a program in a shell. We could do something like laying the buffer content on the shell invocation but
that’s seriously limiting (especially on giant buffers). We could put the content of the buffer in an environment
variable, but it would suffer from the same problem (and damn it’s so dirty). What else?
UNIX systems have this incredible thing called FIFOs. I FIFO — also named pipe — is a special kind of file. It lives on your filesystem (so it’s located at a given path), a bit like UNIX socket. However:
So it implements a rendez-vous buffer between a single reader and a single writer. And yes, the | in your shell is
using something that behind the scene. The thing is: you can create your own, and you don’t even need to write any code.
Enter the mkfifo program. For instance, you can try it out on your own by creating a FIFO (let’s call it rdv),
reading its content with cat first (you’ll see the cat process freeze, so you will need another terminal session!)
and then write content to it, for instance with echo:
mkfifo /tmp/rdv
# in shell 1
cat /tmp/rdv
# in shell 2
echo "Hello, world!" > /tmp/rdv
As seen as the echo starts writing, you can see cat return the result.
Now replace
catwithtail -f😏.
Anyway, Kakoune has a mechanism where it scans %sh{} blocks and it sees $kak_command_fifo and/or
$kak_response_fifo, it will create those FIFOs for us (and manage their lifetimes). Because we are in the shell, we
can write Kakoune commands to execute in $kak_command_fifo, which will be executed as soon as you’re done writing to
the FIFO, and you can read $kak_response_fifo from, for instance, an external program, to get more data from Kakoune.
This is the exact mechanism that is used to stream buffer content between Kakoune and kak-tree-sitter. Here’s the
Kakoune commands used to highlight a buffer:
# Send a single request to highlight the current buffer.
define-command kak-tree-sitter-highlight-buffer -docstring 'Highlight the current buffer' %{
nop %sh{
echo "evaluate-commands -no-hooks -verbatim write $kak_response_fifo" > $kak_command_fifo
kak-tree-sitter -s $kak_session -c $kak_client -r "{\"type\":\"highlight\",\"buffer\":\"$kak_bufname\",\"lang\":\"$kak_opt_filetype\",\"timestamp\":$kak_timestamp,\"payload\":\"$kak_response_fifo\"}"
}
}
I won’t explain the protocol into two much details, but the important part is that I start a shell to run
kak-tree-sitter … -r, format the request (it’s just plain JSON), and ask kak-tree-sitter -r to read from
$kak_response_fifo. You can see in the previous line that we write to it (the write $kak_response_fifo basically
means to write the content of the current buffer to $kak_response_fifo, which is a file — everything is a file in
UNIX!).
kak-tree-sitter -r is made in a way so that it exits quickly. It will read from the FIFO file and forward the request
to the kak-tree-sitter server, which, in turn, will move the request to a different thread so that it can quickly
mark the request done. The whole thing is then synchronous, but extremely fast. The only bottleneck we have here (and
mind it, it’s important) is that we must read from the FIFO synchronously in the shell kak-tree-sitter -r.
I’ve been running with this setup for weeks now, since I started kak-tree-sitter around the end of April, 2023. The
code you read above runs in a couple of Kakoune hooks (the same kak-lsp is using or very similar), which waits for a
idle time after the buffer is edited (in practice, 50ms after the last edit operation). And it’s already fast enough.
However, there is a nice optimization that we could do here. See, the only reason to start a shell is to format the
request to send to kak-tree-sitter, the server. We can completely by-pass it. Instead, we could:
kak-tree-sitter, when a new Kakoune session is created.kak-tree-sitter. No shell is needed to write to files!kak -p.This on its own should greatly enhance performance, which is already pretty good, even with the shell. I plan on working on this enhancement in the upcoming days.
Well, there is one thing. See, in order to support tree-sitter, I had to write this kak-tree-sitter server, and
write buffer contents to FIFO files. kak-lsp is doing the same. All of that is synchronous, and even if it’s fast,
it’s still two sequential locks of the buffer, copy of the buffer, etc. On my side, I’m currently exploring the
%val{history} expanded variable, which contains the textedit operations (so that I don’t have to stream a full
buffer and diff in kak-tree-sitter anymore, but instead just small chunks of updates).
However, the problem still holds. The design of Kakoune is nice when you’re working alone with your small integration, but when you have two big ones (LSP and tree-sitter are not small environments), I think Kakoune shows its limit.
We – @krobelus and I – discussed that matter. A middleware program or a change in how Kakoune interfaces with external programs, especially for buffer streaming, would be greatly appreciated. But it’s not currently the case. And even if we decide to come up with a middleware – that could act as a buffer cache / map, for instance – we would still be writing buffer contents to different FIFOs.
Another thing that I’ve been thinking about a lot is… is this the right approach? Something like tree-sitter seems
to be ideal as an embedded library, directly inside the code of your editor. Especially since Kakoune is all about
selections, being able to have semantic selections by default seems like something pretty good. tree-sitter requires
some runtime resources (relocatable objects like .so / .dylib, queries, etc.)… and it’s the same thing with the
regular Kakoune highlighters (by default, Kakoune doesn’t have any; they come bundled up with your distribution).
I’m not entirely sure the approach scales very well. Of course, I still think the design is excellent, and it prevents
too much maintenance on Kakoune, which is also a good thing. For instance, this runtime resource problem is something
I’m solving in kak-tree-sitter (and easing with a controller tool called ktsctl, which can download, compile and
install grammars and queries for you), but still. The current version of kak-tree-sitter only supports semantic
highlighting, not text-objects just yet (but it shouldn’t be too hard to add). The state of the project is not completly
ready for people to jump in (the wiki is not written), but if you tag along on Matrix, I can help you get started.
Please consider kak-tree-sitter as an experimental project, because I’m not sure this is the right approach, even
though it’s probably the only one for Kakoune for now (I don’t think @mawww plans on
integrating it into the core of Kakoune).
Keep the vibes!
]]>*.txt, because one would want only specific files — like {foo,bar}.txt for instance.
Most of the time, if you are a CLI/TUI enjoyer, you will likely have a similar workflow to this:
ls to show the directories. You will locate the first directory you want to go to.cd <that-dir>.ls again to show what’s there.cd <another-dir>.I don’t think I need to debate on the fact that this is a very time-wasting workflow, and honestly not very
pleasant. Most shells today allow you to do something like cd <tab> and have a way to virtually visit the
directories, until you find the one you want. Press <return> and your command is completed. You can use the
same method for tar-ing files, removing files, running arbitrary commands, etc.
Another interesting way of doing that is to use a fuzzy finder, like fzf or skim. I would usually just do
cd ^t. That works when you know exactly what you are looking for, which is not exactly the situations we are talking about here.
The cd example is what motivated me to make a small tool that I named flirt, but along the way, I realized
it could do much more interesting things, without expanding its scope and not violating the UNIX philosophy.
I never liked file browsers, like nnn, lf or yazi. They look cool, but there is something too much about them. I like the Kakoune’s way. I think the UI from file browsers is interesting, but I think we could use it in a much more barebone/simple (and UNIX) way. The idea is to use it as a read-only UI, and let other tools compose around it.
For instance, let’s take the cd example from before, where we go to a directory by interleaving cd and ls
commands. What do we want to actually do?
cd into it.flirt allows to implement the first part. You can do something like cd $(flirt) — or cd ^s for a shell
shortcut integration, which I highly recommend — and move around directories until you have found the one
you are interested in. Quit flirt, and you have your CLI set to cd <path>. Just press <return> to jump
to that directory.
This is a simple workflow, because it allows you to think of flirt just as a read-only view into your
filesystem. But it can do much more.
flirt can select multiple files. Why would you want to select many files? Well, imagine that you want
to select a bunch of files and put them in a directory. It’s a pretty cumbersome operation to do manually
on the CLI, because you have to type a lot of text to just pick the files you want to feed your mv command.
With flirt, you can do something like mv ^s and pick the files you want.
Because flirt is ordered — you can see the order on the right-side view and switch between the left and right
views with the <tab> key by default — you can ensure the destination directory is the last argument — interactively.
Have you heard about moreutils? It’s a collection of UNIX tools that go a bit further in terms of composability. One
of those tools, vipe, is a pipe editor: it reads from stdin and pipes its content to your $EDITOR. Once you have
finished editing your buffer and quit your editor, it will pipe the modified data back into its stdout:
cmd1 | vipe | cmd2
Composing with flirt is super easy and you can implement interesting workflows, such as with:
flirt | vipe | sh
Which allows you to select a bunch of files on which operate commands — that you add in your editor, via vipe — and
execute the output script!
If you are interested, you can find flirt here, along with some additional links:
If you have cargo installed, you can install flirt easily:
cargo install flirt
I might add a
PKGBUILDfor Archlinux at some point.
Do not hesitate to send your feedback on the discussion mailing list! Keep the vibes!
]]>This blog post sums up what I did with AoC#18, my thoughts about the puzzles and even a meta-discussion about programming challenges. I used Haskell for most challenges and switched to Rust for no specific reason. Just fun.
Also, because the puzzles’ texts are somewhat long, I will just give a link to the text instead of copying it and will just make a very short description of the problem each time.
Finally, don’t expect to find all the puzzles: I stopped at day 15. I will explain the reasons why
at the end of this article. My adventure can be found
here. Most puzzles have input data. You will
find them in day directories as input.txt files.
Enjoy the reading!
If you want to try and take the challenges, I advise you not to read any further as this article would spoil you solutions! You will need a Github account and… a lot of time.
This puzzle is about summing arrays and finding duplicates (without counting).
First puzzle I discovered and first AoC challenge I ever took, I was surprised at the simplicity.
Basically, given lines containing a single number preceded by either + or -, we must compute
the sum of all the numbers.
The first thing to do is to parse the file into a stream of numbers. In Haskell, this was almost a one liner:
main :: IO ()
main = do
numbers <- fmap (map parse . lines) getContents
where
parse ('+':xs) = read xs -- (1)
parse n = read n
If I removed the + from the file, I could even have gone with a one liner:
numbers <- fmap (map read . lines) getContents
The
readfunction is not safe and I don’t recommend using it for real, production projects. In the context of this challenge, though, it was alright.
I don’t have much to say about this puzzle as I found it not very interesting.
We just want to get the first cumulative sum that appears twice. That is, each time a new number
is summed, we get a given “current sum”. We add positive and negative integers, so we might end up,
at some time, with the same number. Because we don’t know when that will happen, we need to turn
the input stream into an infinite stream of numbers that repeat over and over. Haskell has the
cycle function that does exactly that: it takes a list and makes it repeat infinitely.
What is great about cycle in Haskell is that it could be implemented in a very naive way and yet
yield good performance – thanks GHC!:
cycle :: [a] -> [a]
cycle x = let x' = x ++ x' in x'
Or, prefered (personal opinon):
cycle :: [a] -> [a]
cycle = fix . (++)
fixis a very powerful function any Haskell should know. Typically, when you want to corecursively build something, it’s very likely you’ll needfix. I advise you to have a look at my netwire tutorial in which I usedfixpretty much everywhere.
There are several ways to fix this problem. I chose to use a set to hold all the known
intermediate sums. Because we’re working with integers, the Haskell IntSet type is perfect for
that. The algorithm is then just a special fold over an infinite list that stops when a sum is seen
twice by looking it up in the integer set:
findFreq _ freq [] = freq -- this shouldn’t ever happen since the list is infinite
findFreq knownFreq freq (change:xs)
| newFreq `member` knownFreq = newFreq
| otherwise = findFreq (insert newFreq knownFreq) newFreq xs
where
newFreq = freq + change
This puzzle requires you to recognize and count characters in strings. For each string, you must check whether they have any letter that appears exactly once and any letter that appears exactly three times. Adding those counts provides a checksum for a list of strings.
I did that in Haskell as well. My idea was to use a count-array of size 26 (since the strings are
only in [a-z]. So the first part of my algorithm is to count, for a given string, the number of
time each letter occurs. This is done really easily with a fold that increments the proper value
value in the array at the index representing the character folded on in the string. That index
is obtained by turning a character into a numerical representation and subtracting it the numerical
value of 'a':
treatHash :: (Natural, Natural) -> String -> (Natural, Natural)
treatHash occurs hash =
let result :: State = foldl' replace initialState hash
in addPair occurs (foldl' incrOccur (0, 0) result)
where
replace st l = accum (+) st [(ord l - ord 'a', 1)]
incrOccur i@(twos, threes) x
| x == 2 = (1, threes)
| x == 3 = (twos, 1)
| otherwise = i
addPair (a, b) (c, d) = (a + c, b + d)
The replace function performs the update in an input vector (point-free function). This will
effectively increment the vector’s value at index 3 if the character is d (because
ord 'd' - ord 'a' = 3. incrOccur, here, is used to fold over the resulting array (Vector) and
compute if a letter appears twice and if a letter appears three times. You will notice that this
function doesn’t sum. For instance, if you have aabb, you get no letter that appears three times
but you have two that appear exactly twice. However, incrOccur will give you (1, 0), because the
puzzle states it should be done this way. Finally, addPair add the number of twos and threes to a
pre-defined input – that will be handy for an outer fold.
The algorithm is then run on the input and we get the final result:
hashes <- fmap lines getContents
let checksum = uncurry (*) $ foldl' treatHash (0, 0) hashes
We transform the standard input into lines – i.e. [String] – and then fold them with our
treatHash function. Finally, uncurry (*) is a little oneliner to simply multiply the left part
of a pair with its right part.
This part is about finding strings that are almost the same and differ and only by one letter. As doing a programming challenge, the first thing I want to find is a solution, not the best solution. I also think people should really learn from this:
Especially on programming challenges, you’ll be very, very unlikely to implement anything more than (1.), because the inputs are often two small to really benefit from a real optimization. Your time is a real important resource, don’t waste it. Measure whether you really need an optimization. I’m not saying that you should be doing something bad because it’s easier. I’m saying that if it takes you N minutes to write a solution that runs in M milliseconds, if you know that you could do it in, for instance, 10N minutes to write a solution that runs in only 0,7M milliseconds for the foreseen milliseconds, will, you’re going to waste your time.
So, in my case, I started with a naive approach that runs in O(N²): comparing all strings to all others:
searchCommon :: [String] -> String
searchCommon [] = "" -- the empty list has nothing in common
searchCommon (h:hs) = search h hs -- for each string, search in all remaining
where
search h [] = searchCommon hs -- exhausted, search with the next string
search h (x:xs)
| almostSame h x = commonLetters h x -- grab the common letters if almost the same
| otherwise = search h xs -- if not, just try the next string in the sub-list
The algorithm is pretty straight-forward. The searchCommon and search functions are mutually
recursive functions that go from, respectively, the whole list of strings to test and the local
tail as we advance. almostSame is defined as follows:
almostSame :: String -> String -> Bool
almostSame = go 0
where
go diffNb (l:ls) (r:rs)
| diffNb > 1 = False
| l /= r = go (succ diffNb) ls rs
| otherwise = go diffNb ls rs
go diffNb _ _ = diffNb == 1
This function is a special zip that short-circuits if it knows there are two many differences.
When both the input strings are exhausted, if diffNb == 1, then only one character has changed, so
the the overwhole function evaluates to True.
The final part we need is commonLetters, that is pretty straight-forward, too:
commonLetters :: String -> String -> String
commonLetters (l:ls) (r:rs)
| l == r = l : commonLetters ls rs
| otherwise = ls -- all the rest is identic, smartass
We construct a list that has the same characters as the input lists as long as they’re equal. As
soon as they differ, we discard the character and just return any of the input lists – they’re
equal. This function only works, obviously, if both the input strings are almost identical (only one
character differs and they have the same length). The otherwise branch is a short-circuit
optimization that prevents us from traversing the whole inputs after the difference.
This problem was the first “challenging” as it required more thinking. The idea is that a very large
area – the text gives us the hint it is at least 1000 inches on each side but truly, we do not
need this information – must be sliced into several rectangular smaller areas (with edges aligned
with X and Y axis). Obviously, some rectangular area might overlap and the aim of the first part of
this problem is to find out the total overlapping area.
My idea was simple: we can trick and change the problem by discretizing it. This is something I
have said already but most of AoC problems have hidden properties and hidden hints. By thinking
more about the form of the solution, the area to find is expressed in inches². The rectangular
areas are all lying on a 2D grid (1000×1000 at least, remember?) and their coordinates are always
natural numbers (they belong to [0;+∞]). Then, my idea was to state that 1 inch² is actually a
single “cell” at any coordinate in that grid – imagine that as a pixel. 4 inches² would be a 2×2
region in that grad (yielding 4 cells).
So instead of using the general idea of an area (M×N for a rectangle which sides are M and N long wide), we can simply break a rectangular area into its most atomic components (1×1 cells)… and sum them up! We will effectively end up with the area of this area.
A more interesting property of my way of solving it: we can now have a big array mapping to each 1×1 cell the number of times a rectangle lies on it. When iterating through all the rectangles, we just break them into a list of 1×1 cells, look ingg up and updating the big count array. Once we’re done, we just have to filter that big array to remove any element which count is less or equal to 1. The remaining elements are 1×1 cells that contain at least two overlapping rectangles – we don’t really care about the number. We don’t actually care about those elements: the length of that filtered array is the area we are looking for, because it is the sum of 1×1 elements!
Converting a rectangular area – called claim in the text – into 1×1 cells is very easy in Haskell:
type Plan = Map (Int, Int) Natural -- the big array I told you
type Claim = (Id, Pos, Area)
type Id = Text -- the “name” of the fabric the rectangular area is about
type Pos = (Int, Int)
type Area = (Int, Int)
claim1x1 :: Claim -> [Pos]
claim1x1 (_, (line, row), (width, height)) =
[(x, y) | x <- [line .. line + width - 1], y <- [row .. row + height - 1]]
As you can see, I chose to use a Map (Int, Int) Natural instead of an array to store the number
of overlaps per 1×1 cell. That enables us not to care at all about the size of the grid (remember
when I told you we don’t need the 1000 hint?).
The function that updates the Plan to take a claim into account is:
checkClaims :: [Claim] -> Plan
checkClaims = foldl' (\p (x, y) -> checkInch x y p) mempty . concatMap claim1x1
checkInch :: Int
-> Int
-> Plan
-> Plan
checkInch line row = insertWith (+) (line, row) 1
Given a list of claims ([Claim]), checkClaims maps the claim1x1 function and concats the
result, yielding a [Pos] list. That list is then folded over with the checkInch x y p function,
that takes an empty map as initial value. checkInch just increment the value found in the map if
it already exists; otherwise, it sets that value to 1.
Finally, we need to compute the area:
overlappingInches :: Plan -> Int
overlappingInches = length . M.filter (> 1)
As I told you, that is crystal clear: it’s just the length of the filtered map.
This part is interesting also: you need to find out the Id of the claim that doesn’t overlap with
any other claim. I will not go into too much details about the algorithm as it’s very similar to the
previous one: instead of storing the number of overlaps by 1×1 cell, we store a Set Id, giving all
claims that are overlapping – we can see that as a more general form of the first part. We also need
a Map Id Natural that maps a fabric and the number of times it overlaps another. The fabric that
doesn’t overlap any other is then easily identifiable within that map: it has its associated value
set to 0:
searchNonOverlapped :: [Claim] -> Maybe Id
searchNonOverlapped claims =
case M.toList filtered of
[(i, _)] -> Just i -- the text supposes there’s only one
_ -> Nothing
where
(_, overlapped) = indexClaims claims
filtered = M.filter (== 0) overlapped -- hello
Aaaah, day 4… I really don’t get why I got so annoyed by this one. It is quite simple in theory. But that made me realize something I don’t really like about AoC: the hidden rules. You will see, if you read this whole article, that some puzzles require you to make very important assumptions about the input – and there’re a lot of assumptions you could make, so you have to make the right ones!
This puzzle is about guards that must protect a prototype manufacturing lab. You are given an unordered list of events that occur about the guards:
The goal of the part 1 is to find the guard that has the most minutes asleep, and especially, find the minute it is asleep the most – we must multiply the ID of the guard by the minute.
Obviously, the first part of this puzzle is to re-order the input to have it chronologically. Here is the setup code:
data Entry = Entry {
timestamp :: LocalTime,
action :: Text
} deriving (Show)
type GuardianId = Natural
data Action
= BeginShift GuardianId
| WakeUp
| FallAsleep
deriving (Eq, Ord, Show)
entryFromString :: Text -> Maybe Entry
entryFromString s = case split (== ']') s of
[timestamp, action] -> Just $ Entry (parse . unpack $ T.drop 1 timestamp) action
_ -> Nothing
where
parse = parseTimeOrError False defaultTimeLocale "%Y-%-m-%-d %H:%M"
The parsing part is not really interesting as it’s just challenge code: nasty but working parsing code. :D
I then re-ordered the input with:
entries <- fmap (fromJust . traverse entryFromString . T.lines) T.getContents
let byTimeSorted = sortBy (comparing timestamp) entries
By the way, that code made me want to tweet about how Haskell is actually pretty easy to read and reason about. Anyway.
The next part of the algorithm is to transform the entries into a list of timed action. I actually decided to stream it so that I could benefit from Haskell’s stream fusion – and because it’s so simple and transparent:
readGuardianId :: Text -> Natural
readGuardianId = readT . T.drop 1
treatEntries :: [Entry] -> [(LocalTime, Action)]
treatEntries = map $ \entry ->
let time = timestamp entry
in case T.words (action entry) of
["Guard", ident, "begins", "shift"] -> (time, BeginShift $ readGuardianId ident)
("falls":_) -> (time, FallAsleep)
("wakes":_) -> (time, WakeUp)
_ -> error "lol"
That is like mixing streaming and parsing at the same time. Then, the core of my algorithm: dispatch the actions by guard. That is mandatory if we want to actually accumulate the “state” of a guardian (when they’re sleeping, waking up, etc.). Otherwise, we get interleaved results.
dispatchActions :: [(LocalTime, Action)] -> Map GuardianId [(LocalTime, Action)]
dispatchActions = go mempty Nothing
where
go guardians _ ((t, action@(BeginShift gid)):xs) =
go (insertAction gid t action guardians) (Just gid) xs
go guardians jgid@(Just gid) ((t, action):xs) = go (insertAction gid t action guardians) jgid xs
go guardians _ [] = fmap V.toList guardians
go _ _ _ = error "dispatchActions: the impossible fucking occurred!"
insertAction gid t action guardians =
M.insertWith (flip (<>)) gid (V.singleton (t, action)) guardians
This is a by-hand fold that just applies the rule of beginning a shift (storing the ID of the guardian that went napping so that we can correctly dispatch the remaining events).
Then the tricky part:
type Minute = Natural
type Minutes = [Minute]
minutesCounts :: [(LocalTime, Action)] -> Minutes
minutesCounts = go zeroMinutes Nothing
where
zeroMinutes = replicate 60 0 -- (1)
asMinutes = todMin . localTimeOfDay
-- the guard was sleeping
go minutes (Just sleepTime) ((t, action):xs) =
case action of
BeginShift _ -> go minutes Nothing xs
FallAsleep -> go minutes (Just t) xs -- not sure if that would even occur in the input
WakeUp -> go (addSleepCount minutes (asMinutes sleepTime) (asMinutes t)) Nothing xs
-- the guard was awake, so we’re only interested in when they go to sleep
go minutes Nothing ((t, action):xs) =
case action of
FallAsleep -> go minutes (Just t) xs
_ -> go minutes Nothing xs
go minutes _ [] = minutes
addSleepCount minutes sleepTime t = zipWith (+) minutes range -- (2)
where
-- this function is a bit hacky but it generates, for a given range of time, a list of 60
-- elements where the time period has 1 and all the other has 0 (I leave you to the
-- exercise of making that a better function)
range :: Minutes
range = replicate sleepTime 0 <> replicate (fromIntegral t - sleepTime) 1 <> replicate (60 - t) 0
This big function generates a list which length is 60 – mapping the number of times a guard has
passed sleeping at a given minute from midnight to 1:00 AM (see (1) and (2)).
Finally, what we need is a way to compute frequencies – or counts. That is, given a list of anyting, compute that number of time a given anything happens in the list. I wrote a small utility function for that – I got inspired by [@jle], thanks!:
freqTable :: (Ord a) => [a] -> Map a Count
freqTable = M.fromListWith (+) . map (,1)
Then, finding the guard that has slept the more and the minute is easy:
findMostOccurring :: Map a Count -> (a, Count)
findMostOccurring = maximumBy (comparing snd) . M.toList -- and Haskell is hard?! ;)
findSleepiest :: Map GuardianId [(LocalTime, Action)] -> (GuardianId, (Minute, Count))
findSleepiest =
fmap (findMostOccurring . freqTable . spanIndex) . maximumBy (comparing $ sum . snd) . M.toList . fmap minutesCounts
where
spanIndex = concatMap (\(i, x) -> replicate (fromIntegral x) i) . zip [0..]
We first find the guard that has the most time asleep (maximumBy (comparing $ sum . snd). Then,
we find the minutes at which they were asleep the most (findMostOccurring). We are given the guard
ID, the given minute and the number of times they were asleep at that minute. Yay!
For this part, we would like to know which guard is most frequently asleep on the same minute? We already have written all the code needed for that:
findMostFrequentlySleepy :: Map GuardianId [(LocalTime, Action)] -> (GuardianId, Minute)
findMostFrequentlySleepy =
fmap findMin . maximumBy (comparing $ maximum . snd) . M.toList . fmap minutesCounts
where
findMin = fst . maximumBy (comparing snd) . zip [0..]
Instead of summing, we find the maximum time a guard was asleep. Pretty easy.
That puzzle is very natural to solve in Haskell. You are given an ASCII string that contains only letters (lower case and upper case) that represent polymers. You must compute their final reduction by following some basic rules:
aA and Bb cancel each other but not aa nor BB.AbBa will first get
reduced to Aa because aB will cancel out, but then Aa will also cancel out and we will be
left with nothing.You must give the number of units left in the final reducted polymer after all reductions have occurred.
As I said, that is very simple and elegant in Haskell:
reduce :: String -> String
reduce = go []
where
go [] (x:xs) = go [x] xs
go a [] = a
go (a:xs) (b:bs)
| not (isLetter b) = go (a:xs) bs
| (toLower a /= toLower b) || (isLower a && isLower b) || (isUpper a && isUpper b) = go (b:a:xs) bs
| otherwise = go xs bs
I decided to use a zipper-like traversal. My idea is the following:
This algorithm allows me to reduce by doing a forwards-and-backwards kind of sweeping, yielding nice performance. Also, notice that the resulting list is reversed because of how we accumulate the seen characters. Because we don’t care about the order, we will not reverse it back to its original order.
The result is just the length of the output list.
This part asks us to find the polymer that is the smaller if we remove one kind of unit (a single
letter type). So if we remove a for instance, we must remove all a and A.
As there’re only 26 possible solutions (from a to z), and because my solution to part 1 was
already fast, I decided to go brute-force with this one: reducing the input string without a’s,
reducing the input string without b’s, reducing without c’s, etc. And then simply take the shorter
one.
bruteForce :: String -> Int
bruteForce polymers = minimum (map length allReduced)
where
types = ['a' .. 'z']
allReduced = map (\c -> reduce $ filter (\x -> toLower x /= c) polymers) types
Winner winner chicken dinner.
That puzzle was one of the funniest I did. The idea is that, given an infinite 2D map, you are given
a list of several points of interest (POIs) in the form of (x, y) coordinates. The goal, for
this first part, is to find the largest zone in which all points have the same POI. What it means
is that, given several POIs, every positions on the map has a nearest POI (it can have several if
it’s at equal distance to several POIs – those must be discarded by the algorithm so that they do
not count into any zone). Several positions with the same nearest POI and adjacent to each others
form a zone, so that anyone in that zone knows that the nearest POI is the same accross all spanning
positions of the zone – you can picture the zone easily as discs centered on the POIs, but deformed
by other POIs.
The tricky part is that the map is infinite and POIs are scattered around it.
My idea was based on something I do a lot on my spare time with my 3D projects: compute AABBs. An AABB is an enclosing box in which all points lie. The idea is that its size must be as minimal as possible. An AABB, which stands for Axis-Aligned Bounding Box, is the minimal bounding volume that enclose a set of points and which has its edged aligned with the axis (in our case, the X and Y axis). By the way, an AABB in 2D is also called MBR, which stands for Minimum Bounding Rectangle. I chose AABB instead of MBR because I’m more used to work in 3D, but they’re essentially the same thing.
So, the first thing I wanted to do is to compute the AABB of all the POIs for a two reasons:
Ok, let’s see my code:
type Point = (Int, Int)
-- Note to readers: this the way I like to encode AABB because it eases some operations on points.
-- The lower point is a point that belongs to the AABB that satisfies the rule that no other point
-- with different coordinate are lower than it. Same thing for upper. I also like that encoding
-- because generating an AABB from a set of points is trivial.
data AABB = AABB {
aabbLower :: Point,
aabbUpper :: Point
} deriving (Eq, Show)
findAABB :: [Point] -> AABB
findAABB [] = error "nein" -- this will never be called, so we don’t care about type safety here
findAABB (a:ls) = foldl' updateAABB (AABB a a) ls
where
updateAABB (AABB (lx, ly) (ux, uy)) (x, y) = AABB {
aabbLower = (min (min lx x) lx, min (min ly y) ly),
aabbUpper = (max (max ux x) ux, max (max uy y) uy)
}
-- This function gives me a list of points that are in the AABB. It actually gives me all the points
-- the AABB wraps.
aabbToStream :: AABB -> [Point]
aabbToStream (AABB (lx, ly) (ux, uy)) = [(x, y) | x <- [lx .. ux], y <- [ly .. uy]]
-- Test whether a point lies on any edges of the AABB. You’ll get why this function is important
-- later.
liesOnAABB :: Point -> AABB -> Bool
liesOnAABB (x, y) (AABB (lx, ly) (ux, uy)) = x == lx || x == ux || y == ly || y == uy
I annotated the code with comments so that you can get what it’s for.
So, I create the AABB and then I call aabbToStream in order to get all points. You might already
have guessed the next step: we are going to find the nearest POI to all the points. For this, I
just went naive and just computed the Manhattan distance to all POI and kept the smallest. If we
map that function to all coordinates generated by the AABB, we get the first part of the solution.
manhDist :: Point -> Point -> Int
manhDist (a, b) (c, d) = abs (a - c) + abs (b - d)
nearest :: Point -> [(Int, Point)] -> Maybe Int
nearest p points =
case sortBy (comparing snd) $ map (\(i, x) -> (i, manhDist p x)) points of
[a] -> Just (fst a)
a:b:_ -> if snd a == snd b then Nothing else Just (fst a) -- (1)
_ -> error "nearest"
Here, (1) applies the rule I described earlier about at least two POIs at the same distance: we
just discard the point and it doesn’t participate in creating any zone.
Then, how do we find the biggest area? Easy: we re-use our freqTable function from Day 4 to
compute a frequency table! In my case, I just renamed that function freqs:
freqs :: (Ord a) => [a] -> Map a Natural
freqs = fromListWith (+) . map (,1)
If we call that function on a list of [Int], we end up with Map (Maybe Int) Natural that
gives us the number of positions a given POI is the nearest. It’s perfect, because it’s exactly
what we are looking for!
biggestArea :: [Maybe Int] -> Natural
biggestArea = snd . maximumBy (comparing snd) . M.toList . freqs . catMaybes
Here, catMaybes just remove the Nothing case so that we go from [Maybe Int] to [Int]. We
then find out which POI has the biggest number of nearest positions and we simply return it. Because
those are positions, their sum is then the area of the zone: we’re done. Or almost. Remember that
some zones have an infinite areas. Thanks to the AABB, it’s actually easy to find which ones: those
have at least one point that lies on the AABB’s edges. We just have to iterate through all the
points and black list some points:
blackListPoints :: [(Point, Maybe Int)] -> AABB -> Set Int
blackListPoints points aabb = foldl' blacklist mempty points
where
blacklist blist (p, Just i) = if liesOnAABB p aabb then S.insert i blist else blist
blacklist blist _ = blist
The part 2 asks something different: now we want to find the area of the region containing all
locations for which the total distance to all POI is less than a given constant (10000). My
solution was actually way easier than expected, surprisingly:
safeArea = filter (\p -> sum (map (manhDist p) coords) <= 10000) points
Done. :)
Here we go again: graph theory. Fortunately for us, it’s not a hard graph puzzle. That first part is to simply display a string that shows the order in which a graph must be traversed. If two nodes can be traversed at the same time, the node which letter comes first alphabetically is traversed first.
I’ll just show the traversal because the rest is not really interesting for that puzzle:
-- The graph encodes the relationship in reverse: it maps each node its list of dependencies.
-- So if we have something like A -> [], it means that the A node doesn’t have any dependency.
type Graph = Map Step (Set Step)
type Step = Char
-- Get the list of all available steps; i.e. they don’t have any dependency.
getAvailable :: Graph -> [Step]
getAvailable gr = [step | (step, set) <- M.toList gr, S.null set]
-- Traverse the graph and get the ordered steps to go through.
stepAvailable :: Graph -> [Step]
stepAvailable gr = case sort (getAvailable gr) of
[] -> []
(s:sx) -> s : stepAvailable (removeStep s gr)
removeStep :: Step -> Graph -> Graph
removeStep s = purgeDep s . M.delete s
where
purgeDep = fmap . S.delete
It’s a typical functional problem that gets solved very easily in Haskell.
The second part is pretty interesting. Instead of stepping through all the steps sequentially, you ar given a pool of workers. It will take a given amount of time for a given worker to complete a task and able us to visit a given node in the graph. We have to guess how many time it will take to complete all of the steps.
I won’t post the code (it’s on GitHub if you want to have a look at it) as it’s a bit boring and the idea of my solution is enough. The concept is to have a stepped simulation (i.e. you perform a set of action in a given “round”, then repeat). In my case, each round is composed of several steps:
0, then it’s done, otherwise it’s still running.This part was interesting because it made me write a parallel graph traversal (a graph task scheduler) that could be used as base for a real and parallelized (I/O) task scheduler. Interesting stuff.
In its simple form, this puzzle is not really interesting and I could definitely present you the solution in a single paragraph. However, I found it pretty fun to do so I’ll go a bit further.
The problem is the following: we are given a list of numbers that represent a data structure. That data structure is basically a tree with tagged metadata. The goal is to parse the list of numbers to generate a memory representation of that tree and compute checksums on it. The structure is:
The file format is made so that the data are nested. I’ll copy and explain an example:
2 3 0 3 10 11 12 1 1 0 1 99 2 1 1 2
A----------------------------------
B----------- C-----------
D-----
Here, only the first line is present in the input file. The first 2 means that the first (A)
node has two children (we don’t know anything about them yet) and the 3 means it has three
metadata. Those are the header. Then, since it has two children, the next 0 is the start of the
header of its first children (B), which has no child and three metadata (3). The next 10, 11
and 12 are then those metadata (since it doesn’t have any child). This node is then complete. If
you go back up in the tree, you know that A still has another child. So the next number, 1, is
the number of child of C and 1 its number of metadata. The next number 0 is the number of
child of D and it has 1 metadata, which is 99. C, as seen above, has one metadata, so 2 is
C’s metadata. Then, since A has three metadata, 1, 1 and 2 are its.
Pfiou. Seems hard to read for a human, right? However, if you’re used a bit to recursive data structure and more specifically recursive parsers, this kind of encoding is actually pretty neat!
Let’s go and implement the parser of that tree. First, the structure. We will not need the header in the output (it’s only used for parsing), so we will not encode that directly (it’s still available as the length of the list of children and length of the list of metadata entries):
data Node = Node {
nodeChildren :: [Node],
nodeMetadata :: NonEmpty Natural
} deriving (Eq, Show)
Pretty simple, right? This is a self-recursing data structure that is pretty simple and basic to any functional programmer.
The
NonEmpty adata type, in Haskell, is a list that cannot have zero element. That is enforced at compilation as it’s impossible to create such a list without explicitly giving at least one element. All the operations defined onNonEmpty arespect that rule (for instance, removing an element from it might not remove anything if it has only one element – otherwise you’d break its invariant and Haskell is not Javascript. Lol.
So, how do we parse that? I’ve already spoiled you the solution: we need to implement a recursive parser. I know that because I’ve been using parsec for like 7 years now, so I’m pretty used to that kind of parsing and as you use it, you will quickly recognize when you can use such an idiom.
However, instead of using parsec directly, I will implement it myself with some very basic types
every Haskellers know – if you don’t: go learn them! I’ll be using the State type only, which is
basically just a recursive function used in a monadic fancy way:
-- A possible representation of the State monad is just a function that takes a value 's' and
-- returns a new, altered 's' (we call that a state, but as you can see, it’s completely pure code,
-- no mutation happens at all) and an output value 'a'.
data State s a = State { runState :: s -> (s, a) }
The real
Statetype is defined in the mtl Haskell library.
So here, you have the impression or illusion of s being a state, but what it truly is is just
a value that is passed around to a recursive function – and plot twist: recursion is the way to
implement locally-defined states, but I will not explain that (read my netwire tutorial and the
Auto type, if you are interested).
The functor / monadic part:
instance Functor (State s) where
fmap f = State . fmap (fmap f) . runState
instance Applicative (State s) where
pure x = State (, x)
p <*> q = State $ \s ->
let (s', f) = runState p s
(s'', a) = runState q s'
in (s'', f a)
instance Monad (State s) where
return = pure
q >>= f = State $ \s -> let (s', a) = runState q s in runState (f a) s'
All of this can be generated automatically by GHC with
derivingdata annotation.
And some combinators we’ll need:
-- Get the current value of the “state”.
get :: State s s
get = State $ \s -> (s, s)
-- Get the current value of the “state” with a function pre-applied to it.
gets :: (s -> a) -> State s a
gets = flip fmap get
-- Change the value of the “state” by applying a function to the state.
modify :: (s -> s) -> State s ()
modify f = State $ \s -> (f s, ())
-- Just a convenient method to just get the output value and discard the final state. You need the
-- initial value to use as state.
evalState :: State s a -> s -> a
evalState st = snd . runState st
So basically, since this is a very basic and simple code (I think all Haskellers should write that
in their first month using Haskell, it’s a good exercise), I just included the mtl library and
used its State type to write my recursive parser.
This is my parser:
newtype Parser a = Parser { runParser :: State [Natural] a } deriving (Applicative, Functor, Monad)
So basically, a Parser a generates value of type a and maintains a list of Natural around.
Those Natural are the numbers from the input we are going to parse. Let’s write the actual parser
now.
-- Turns the (string-encoded) list of numbers and generates the root node, that contains all of
-- the children.
parse :: String -> Node
parse = evalState (runParser parseNode) . map read . words
-- Read a single number from the input and consume it from the state.
readInput :: Parser Natural
readInput = Parser $ gets head <* modify tail
parseNode :: Parser Node
parseNode = do
-- We read the two first numbers (header)
childrenNb <- readInput
metadataNb <- readInput
-- Recursive parser! The NE.fromList is an unsafe function that is used for convenience for this
-- puzzle part.
children <- replicateM (fromIntegral childrenNb) parseNode
metadata <- fmap NE.fromList (replicateM (fromIntegral metadataNb) readInput)
pure $ Node children metadata
As you can see, the parser code is extremely simple with a recursive combinator parser! And we’re actually done for the first part. The checksum is simple and is:
checksum :: Node -> Natural
checksum node = metadataChecksum (nodeMetadata node) + sum (map checksum $ nodeChildren node)
metadataChecksum :: NonEmpty Natural -> Natural
metadataChecksum = sum . NE.toList
The second part is not interesting as it just requires a new method to compute the “value” of a given node:
nodeValue :: Node -> Natural
nodeValue (Node [] metadata) = metadataChecksum metadata
nodeValue (Node children metadata) = sum [nodeValue n | Just n <- map index (NE.toList metadata)]
where
index i =
let i' = fromIntegral i - 1
in if i' < 0 || i' >= length children then Nothing else Just (children !! i')
This puzzle was the first one when I decided to go full Rust! All the remaining puzzles were solved in Rust – if you were reading only for Haskell, sorry for your loss. :(
This puzzle is not really interesting as it’s just a fancy algorithm that adds element to a collection and sometimes removes from it. There was a trick, though: the second part requires to run our algorithm on an input that was a hundred times larger.
The typical trap is that when you add value in the middle of a collection, the complexity in terms of memory and CPU can largely vary. Everything depends on what you do. For very rare additions / deletions, it’s possible that you can accept O(n) complexities. However, if you often insert stuff, you might want something else. In the same spirit, some data structure can efficiently add in O(1) at the beginning or end of the collection or might require a complete copy.
Even though the puzzle is not interesting in itself, it reminds us how crucial and critical it is that a programmer must know what structure to use depending on the inputs and operations that will be performed on the data. In our case, we are going to add and remove a lot at arbitrary places in the collection. Vectors are really bad candidates at that kind of operations, because they will require a complete scan of the right part of the collection, which is O(n), every time you add or delete something (to shift right / left, respectively). This is bad. Vectors are also bad when you want to add at its beginning (it requires the same right shift as the random case).
Double-ended queue (VecDeque in
Rust) are a solution to the problem to insert at the beginning. That insertion is O(1) amortized.
My solution to this was to use a zipper to stay focus on a given number in the collection but also “move around” in O(1). The idea is the following:
struct Zipper {
left: VecDeque<isize>,
middle: isize,
right: VecDeque<isize>,
}
When you want to move to the left, you just take the middle number and push_front it to the
right double-ended queue and you pop_back the left one and that value becomes the new
middle. You do the opposite thing to go to the right.
To insert an element at the current location, you just push_front middle to the right and then
middle is assigned the new value. To remove, you just pop_front right into middle.
Then, the all puzzle is just adding and removing according to a predicate. Since all the operations
I mentioned above run in O(1) amortized (they might allocate if the buffer is too small), we will
not suffer from the typical O(n²) complexity a Vec implementation has.
This is the kind of problem I suck the most at. Not because they’re hard. Because they’re easier than expected. As an engineer, I tend to overthink about the context, the input’s hidden properties, the possible errors, the heuristics, what could go wrong, etc. On a regular job basis, that is actually a good thing – it’s better to foresee things than to have to repair them. However, at a given extreme, that way of thinking will make you go nuts and will make you think of an easy and straightforward problem as a complex and convoluted one. I know that and the main thing my mind does when solving a problem is to think about how really hard a problem is. I just completely failed on this one, haha. :D
So, you are given a short list (~360) of 2D points. You know nothing about how they’re spread nor the limits they lie in. You only have ~360 points on a 2D plane. Those points, however, come with two attributes:
-∞; +∞]).-∞; +∞]).So basically, each point starts at a given position and goes into a straight line forever. The unit
of the velocity is not known and is not really needed – even though it might be unit/s.
Nevertheless, the text gives the hinting that, at a given (unknown) time, the points form a message
that can be visually witness. The goal is to give the message.
In the first 15 minutes, I went through several thoughts. “Whoa, they want us to write an OCR?! Really?!”. Nah, you dumbass. Since we all have a unique input (and hence, a unique expected output), we don’t have to write an algorithm that can recognize any text. We just have to get to visualize our input.
However, when does the text appear? At t = 0, we have a large cloud of spread points. We don’t
know when the text will form. Also, the challenge explicitly states that the text forms only once:
the points will never gather into text afterwards. We must not miss it then.
My idea was that to find hidden properties of the overall text first. By being able to extract a useful information telling me whether or not I’m far or close from having a visualizable text, I was able to run a loop-like simulation, moving each points by its respectiv velocities, until that hidden information reaches a local minimum. As an engineer, I was annoyed by that, because I had no idea whether the first local minimum was the right one – the puzzle’s text doesn’t state anything about that and I had not found any information to help with that in the input. I could also use the wrong criteria (maybe we’re looking for a local maximum?). I got stuck with those ideas for long minutes.
Finally, I decided to implement a specific criteria:
When I ran that loop, I got the first local minimum in 10011 seconds. Clearly, if you tried to actually run that simulation with the real time, you’d be waiting for a long time – 10011 seconds is 2 hours, 46 minutes and 51 seconds.
The size of the AABB at t = 10011 was also pretty small (around 60×60). I then decided to
display the message directly in the console. In order to do that, I had to transform my 2D points
(expressed in the natural ℝ² basis we use in world space coordinates) into a space that I could
easily use to display (basically, [0; w] and [0; h]). That transformation is done with the
following code:
// The rendered “map”
let mut lol = vec!['.'; w as usize * h as usize];
for p in points {
let x = (p.position.0 - aabb.lower.0) * (w - 1) / w;
let y = (p.position.1 - aabb.lower.1) * (h - 1) / h;
let o = x + y * w;
lol[o as usize] = '#';
}
Then, we just need to iterate on all the points and render them to the terminal to finish the challenge:
for row in 0 .. h {
for col in 0 .. w {
print!("{}", lol[(col + row * w) as usize]);
}
println!("");
}
Part 2 was almost a joke: we were asked to give the time at which the text appeared. As this was a
hidden property to find in the first place, completing part 2 took a few seconds: 10011.
A pretty common algorithm to implement: sliding window. Basically, you are given a matrix of numbers and you have to compute several sums using a sliding kernel which size is 3×3. The size of the matrix is 300×300 and you just want to compute the biggest 3×3 square (and give its index in the matrix as row / column).
This was my solution:
let mut largest = (0, i8::min_value()); // (index, power)
// 298 is 300 - 2: we want to stop there so that the 3×3 square won’t overflow
for row in 0 .. 298 {
for col in 0 .. 298 {
let mut power = 0;
// sum the square
for i in 0 .. 3 {
for k in 0 .. 3 {
power += grid[index(col + i, row + k)];
}
}
let i = index(col, row);
// if its power is any larger, store it along with its index
if (power == largest.1 && i < largest.0) || power > largest.1 {
largest = (i, power);
}
}
}
println!("Largest fuel cell: ({}, {})", 1 + largest.0 % 300, 1 + largest.0 / 300);
That’s pretty much it. Second part is more interesting.
For this part, the problem changes a bit: we still want to sum squares, but we want to get the find the square that has the largest total power of any size comprised between 1×1 and 300×300 – we want its index and its size.
That problem can be solved in several ways, with different complexities. It’s easy to see that you can quickly go with a bad complexity if you decide to refactor the previous algorithm to take a dimension (that will be squared) and call it 300 times. Maybe that would be enough.
However, I wanted to implement something smarter on this one. It’s easy to see that a lot of spanning squares will overlap. For instance:
+-+-+-+-+-+
|0|1|2|3|4|
+-+-+-+-+-+
|6|7|8|9|A|
+-+-+-+-+-+
|B|C|D|E|F|
+-+-+-+-+-+
If you consider the first, top-leftmost 2×2 square:
+-+-+
|0|1|
+-+-+
|6|7|
+-+-+
And the top-left-mostmost 3×3 square:
+-+-+-+
|0|1|2|
+-+-+-+
|6|7|8|
+-+-+-+
|B|C|D|
+-+-+-+
You can see that a the smaller one is included in the bigger one. What it means is that each spanning square is a partial sum to spanning square of a higher dimension. My algorithm benefits from that in order to reduce the number of elements to sum at each given dimension.
Also, another thing I did that suprised people on IRC: I reversed the way the algorithm works in terms of traversal. Instead of traversing dimensions and then traversing the grid (for all dimensions then for all squares), I traverse the grid and then I traverse the dimensions (for all square in the grid, for all the dimensions). This gives me a more natural way to write the partial sums in my code.
Finally, I also work out some formalæ to know “what’s the biggest dimension we can go up to given
the current grid cell.” Yeah, think twice: when you want to go through all dimensions from the
top-leftmost cell, you will be able to sum squares from 1×1 up to 300×300. But which dimension
can you go to when the starting (1×1) cell is in the middle of the grid? This is actually pretty
easy. The formalæ can be found very quickly by thinking in terms of size of the grid (300×300) and
the index of a grid cell. The biggest dimension is just the minimum of the maximal allowed row
iterations and the maximal allowed column iterations. You can picture that mentally by “which edge
I am the closest to?”. For rows, it’s simply 300 - current_row and for columns,
300 - current_column. The minimum value gives you the maximal spanning dimension you can go up to.
Finally, a word on how the partial sums are created: when you start computing the sum of the N
dimension, you already have the partial sum of dimension N-1 (the algorithm starts with the first
dimension set to a given value). Then, instead of summing N² element, since you already have the
sum of (N-1)², you just need to sum 2 × (N - 1) + 1 values. If you’re not convinced, at
dimension 278, 278² = 77284 sums while my algorithm is 2 × (278 - 1) + 1 = 555 sums. It’s
around 139 times less.
In my code, I do that by adding – relative to the previous spanning square – the right column
(which size is N - 1), the bottom line (N - 1 as well) and the single element in the diagonal.
Hence 2 × (N - 1) + 1. And that completes a new partial sum, that will be used for higher
dimensions!
Here’s just a very quick schema to show you how to compute the sum at dimension 5 by using the
sum of the spanning square of dimension 4 – · is already computed and R are the right column,
B the bottom line and D the element in the diagonal:
+-+-+-+-+-+
|·|·|·|·|R|
+-+-+-+-+-+
|·|·|·|·|R|
+-+-+-+-+-+
|·|·|·|·|R|
+-+-+-+-+-+
|·|·|·|·|R|
+-+-+-+-+-+
|B|B|B|B|D|
+-+-+-+-+-+
So, here’s the code:
let mut largest2 = (0, i64::min_value(), 0); // (index, power, dimension)
// for all rows…
for row in 0 .. 300 {
let max_iter_row = 300 - row; // 300 -> 1
// for all columns…
for col in 0 .. 300 {
let max_iter_col = 300 - col; // 300 -> 1
let max_dim_squared = max_iter_row.min(max_iter_col); // 300x300 -> 1x1
// power used for nested dimensions
let mut nested_power = grid[index(col, row)] as i64;
// note: we don’t have to compute the first dimension because it’s set right away to the given
// nested power
// for all dimensions up to the max
for d in 1 .. max_dim_squared {
let mut power = nested_power;
// compute the 2 × (N - 1) elements
for k in 0 .. d {
power += grid[index(col + d, row + k)] as i64;
power += grid[index(col + k, row + d)] as i64;
}
// add the diagonal
power += grid[index(col + d, row + d)] as i64;
let i = index(col, row);
if (power == largest2.1 && i < largest2.0) || power > largest2.1 {
largest2 = (index(col, row), power, d + 1);
}
nested_power = power;
}
}
}
println!("Largest fuel cell of all: ({}, {}, {}, of power {})", 1 + largest2.0 % 300, 1 + largest2.0 / 300, largest2.2, largest2.1);
This puzzle looked a bit like the double-ended queue one from day 9. The extra bit of information is that you now have to apply a pattern on several values to know how they should be mutated. Given a list of flower pots and some rules that give you how a pot should grow flowers (or not) according to the state of itself and its neighbors, the goal is to predict the sum of the pots (their index in the list) for all pots that contain flowers after 20 generations.
In the first place, I had to recognize that I needed a double-ended queue. As always, the puzzle’s text doesn’t explicitly tell you that the pots at leftmost and rightmost positions can “spawn” new pots by applying the rules on empty pots (infinite). I was confused at that for a while.
My encoding of rules is pretty simple and wasteful: since a rule gives you a pattern (which pots)
and an output (should have flowers / shouldn’t), a single byte should be enough for that (the length
of a rule is five: it gives you the state of the two left neighbors, the state of the current pot
and the state of the two right neighbors). However, I encoded those with a simple array of five
binary states (Rule::Empty and Rule::Pot).
In order to apply a pattern, we must retreive the current pot and two at its left position and two at its right position (if pots are missing because we’re at edges, we must spawn empty pots with negative / positive indices). Then we can just apply the pattern by looking it up in a hashmap: we get the next generation value.
Nothing really interesting code-wise to show here.
Part 2 is funny. We’re told that instead of finding the sums after 20 generations, we need to find it after fifty billion (50000000000) generations. Obviously, trying to run the above algorithm for 50000000000 generations will take ages, so we need to find a better way.
My first inital idea was that if I took a look at the actual sum value at each generation, I could – perhaps – see some kind of patterns. At first I was looking for cycles and hence cycling sums. I then run my algorithm and had a look at the output data. I was suprised to find that, very quickly, the flowers grow linearily. What it means is that, after a given number of generations, you can guess how many flowers there will be at a given future generation by applying a linear formula (typically, a simple multiplication and addition).
In my case, I noticed that at generation 100, the sum was 6346. At 101, it was 6397. At
102, it was 6448. At 200, it was 16546. You can see the pattern – if you don’t, compute the
difference between the sum at 101 and the sum at 100… and the difference of sum at 102 and
101.
Hence, I came up with the following linear formula:
// O(1) get the score at a given generation – works only for gen ≥ 100.
fn score_at(gen: u64) -> u64 {
6346 + (gen - 100) * 51
}
The actual implementation uses 101 instead of 100 because we want to get the sum after a
given number of generations, not at.
That kind of linear optimization was really fun to write yet a bit tricky to find. :)
I think this was the puzzle I enjoyed the most – among the ones I did. The goal is to parse is rails map on which wagons go and make wagons move around by respecting some specific rules: we must find wagon collision is report their positions 2D position.
Parsing is actually pretty simple: the input data is the 2D map that contains the rail system along with the initial position of wagons. About the map, I decided to store only crossings, not the actual rails, because I didn’t need them! So in order to do so, I changed a bit the regular way I encode 2D maps in memory and decided to use a hashmap!
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Rail {
Cross, // +
RampRight, // /
RampLeft // \
}
struct Map(HashMap<(u32, u32), Rail>);
The rest of the code is actually pretty simple: there are several functions, one for moving carts,
one for changing directions at cross, one for detecting collision. A main loop is responsible in
moving carts and checking if there’s any collision. If no collision is detected, we just loop back
up. If a detection is detected, we break the loop and display the position of the crash. The
collision algorith returns the IDs of the carts that collided into each other.
let collision = loop {
// sort the cart by y component
carts.sort_by(|a, b| a.pos.cmp(&b.pos));
let collisions = move_carts(&map, &mut carts);
if !collisions.is_empty() {
break collisions[0];
}
};
println!("First collision: {:?}", carts[collision.0].pos);
The sort_by is needed because of priority rules in the puzzle’s text.
Moving carts and detecting collision is pretty straightforward:
fn move_carts(
map: &Map,
carts: &mut Vec<Cart>,
) -> Vec<(usize, usize)> {
let mut collisions: Vec<(usize, usize)> = Vec::new(); // no collision to begin with
// move and check collision for all carts
'outer: for i in 0 .. carts.len() {
// check that this cart hasn’t been collided into yet
for &collision in &collisions {
if i == collision.0 || i == collision.1 {
// already collided, don’t move that
continue 'outer;
}
}
// move the cart and check if it’s collided into another
move_cart(map, &mut carts[i]);
let collision = find_collision(&carts, i);
if let Some(collider) = collision {
collisions.push((collider, i));
}
}
collisions
}
This code is not really optimized – we redo the same thing very often – but it’s way than enough to solve that puzzle’s part. Finding collision is very simple: we just try to find a cart with the same position.
The tricky part is for moving at cross. The rules state that if you arrive at a cross, you have to turn in a given direction and change the future direction you will take at the future cross, if any. This was encoded inside each cart, so that they have a “memory” of turns to take.
#[derive(Debug)]
struct Cart {
pos: (u32, u32),
dir: Direction,
next_turn: Turn
}
A cart starts by going on its (relative!) Turn::Left, then at the next turn it will go
Turn::Straight, then Turn::Right and finaly will loop back to Turn::Left. Note how different
it is to Direction: a Turn is relative to the current movement of a cart while a Direction is
absolute (at first, I wanted to have North, East etc. for Direction so that confusion is not
possible).
In that part, we want to find the last standing cart, assuming that crashing carts are immediately removed from the map. The code is actually very similar: instead of breaking the loop at the first collision, we break it when there’s only one cart left on the map – we don’t forget te remove the crashed carts!
loop {
// sort the cart by y component
carts.sort_by(|a, b| a.pos.cmp(&b.pos));
for (ci, ck) in move_carts(&map, &mut carts) {
carts = carts.into_iter().enumerate().filter(|&(i, _)| i != ci && i != ck).map(|(_, c)| c).collect();
}
if carts.len() == 1 {
break;
}
};
println!("Last standing cart: {:?} ", carts); // this contains only one cart
Very similar to the double-ended queue puzzle as well, this one doesn’t actually require any deletion, just indexing correctly into a growing buffer. Nothing really interesting to show about this problem, except maybe the way recipes are created.
To create new recipes, the two Elves combine their current recipes. This creates new recipes from the digits of the sum of the current recipes’ scores. With the current recipes’ scores of 3 and 7, their sum is 10, and so two new recipes would be created: the first with score 1 and the second with score 0. If the current recipes’ scores were 2 and 3, the sum, 5, would only create one recipe (with a score of 5) with its single digit.
In order to implement that, I recognized that I will always create at least one recipe: 0 + 0 = 0,
and as soon as I create two recipes, the other one will always be 1, because the maximum value is
9 + 9 = 18 (1 and 8). Here’s my code that gets those two recipe numbers:
fn create_new_recipe(a: usize, b: usize) -> (Option<usize>, usize) {
let s = a + b;
let x = s / 10;
let y = s % 10;
(if x == 1 { Some(x) } else { None }, y)
}
Part 2 is really not interesting as it’s just using ends_with to find a suffix. I’ll let you read
the code if you’re interested.
Aaaaaaaaaand this is the last puzzle I attempted. I actually decided not to finish it because it was taking me time. I will tell you more about that in the conclusion.
The goal is to write a simulation of elves fighting goblins (or goblins fighting elves) and finding paths in a map that has holes / mountains in it. So most of the code to write is about Dijkstra or A*. The puzzle seemed interesting but it was way too much for my spare time to spend on. I advise you to have a look at the insanely long puzzle’s text – that will give you an idea of everything you need to implement in order to get the your solution working.
Ah, my first Advent of Code. It was both interesting, exciting, frustrating and time-consuming. I found several pros and drawbacks:
Pros, first:
And drawbacks:
My general feeling is that it was fun, but I think that I won’t do it next year, because I had to put all my spare projects on hold for that. I didn’t learn anything new – all the algorithms had me write algorithms I already knew, except maybe the partial dimension squared algorithm I “invented”: someone told me that it’s very similar to a real and well-known algorithm! How funny is that! The algorithm is Summed-area table and my solution is, indeed, very similar to it. But the thing is: I came up with the idea, and this is priceless for training brains!
Now I’ll return to my graphics, Rust and over experiment projects of mine! I hope you liked that article, it was a bit long (it took me almost two weeks to write!) but I felt I needed to make it. To… well… scrap and forget about Advent of Code and all my spare time I didn’t use for my own projects. :)
Keep the vibes – especially you, @lqd.
]]>I just added a new feature to warmy to make it more accessible and easy to use. It concerns JSON and serde.
If you’ve been using warmy a bit, you might be used to its traits and structures, like Load,
Loaded, Load::reload, Res, etc. All those concepts are mandatory to implement loading,
reloading and scarce resource sharing. Since version
0.7, warmy has got loading and
reloading methods. That feature enables you to have several impl for the same type of resource by
changing the method used to load. The default is () but you’re free to use any you want. The idea
is that you can call the Store::get_by or Store::get_proxied_by methods to specify which method
to use explicitly when loading and reloading a given resource.
warmy 0.11.1 uses that concept to provide a universal implementor for anything that implements the
Deserialize trait from serde. Basically, once your type implements Deserialize, you can
load and hot-reload values of this type by using the Json type from warmy.
Universal JSON implementors are available only if the
"json"feature is enabled in warmy. It’s the case by default. Feel free to usedefault-features = falseif you don’t want it.

Here’s a short example of what it looks like to load and hot-reload a Dog using the universal JSON
feature:
use serde::Deserialize;
use warmy::{Res, SimpleKey, Store, StoreOpt};
use warmy::json::Json;
use std::thread::sleep;
use std::time::Duration;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
struct Dog {
name: String,
gender: Gender
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
enum Gender {
Female,
Male
}
fn main() {
let mut store: Store<(), SimpleKey> = Store::new(StoreOpt::default()).unwrap();
let ctx = &mut ();
let resource: Result<Res<Dog>, _> = store.get_by(&SimpleKey::from_path("/dog.json"), ctx, Json);
match resource {
Ok(dog) => {
loop {
store.sync(ctx);
println!("Dog is {} and is a {:?}", dog.borrow().name, dog.borrow().gender);
sleep(Duration::from_millis(1000));
}
}
Err(e) => eprintln!("{}", e)
}
}
This feature should help people adopt the crate and use it without worrying too much about the
actual implementation of Load.
If you’re interested in adding other methods, like YAML, XML or whatever, feel free to open a PR on GitHub! I’ll be very happy to accept it. More documentation about the feature can be found in the README or the online documentation.
Keep the vibes.
]]>Oh hello there! It’s been a while since I wrote on here. My last article was posted on February 25th and then I decided it was time to blog more about what I do. Especially, I think I will try to blog more often, even if it’s about trivial things, as long as I share my thoughts.
So today, I’m going to talk about the splines crate. And more specifically, the splines-1.0.0-rc.1 release candidate I uploaded today on crates.io.
Maybe you’re wondering what a spline is, in the first place. A spline is a mathematic curve that is defined by several polynomials. You can picture them mentally by several small and simple curves combined to each others, giving the curve an interesting shape and properties. Now why we want splines is easy to understand: imagine a curve, something smooth and a bit complex (maybe even with loops). Now, imagine you want to make an object move along that curve. How do you represent that curve and how to you “make something advance along it?”
Splines are defined by several “control points”, also known “keys”, “knots” or whatever makes sense to you. Those are just points of interest that will have a direct impact on how the curve will bend and behave. Most of control points have the interesting property that the curve will pass through them, allowing you to define points you want your object to pass by, for instance. Not all control points have that property, though.
Now all the magic happens with those control points: when you want to sample a value at a given control point, you just get back the value carried by this control point… but when you want to sample in between two control points, for instance, you get back an interpolated value. That interpolation is the key to why we want splines. Several interpolation mechanisms exist and I’m sure you know at least two:
t = 0, you get 100% of the first value and 0% of the second value. If you sample at t = 0,5,
you get an equal amount of both value (so you get the mean between both value). At t = 1, you
get 0% of the first value and 100% of the second value. Simple but really useful.Keep in mind that a spline is a just an abstractio mathematical object and that you can use it for lots of concepts and situations. For instance, in demoscene productions of mine, I use splines for camera movement, animation transitions, color-blending and more!
The splines crate has been around for some time now and I received some feedback from people on the GitHub page asking for more features. Basically, the crate can now:
None).f32 but also f64 or whatever).I think something interesting to dissect in this blog article is the last feature about polymorphic sampling types and what I had to do in order to make it happen. Also, this is highly correlated to a design choice I have made months ago: supporting foreign crates to interpolate specific types is done inside the splines crate, not in other crates. I will discuss why just bellow.
Basically, in splines, prior to 1.0.0, a spline was polymorphic type mapping a key of type f32
to a control point of type Key<V>, V being the carried value. You then manipulate a Spline<V>.
The problem with that is whenever someone wants to use a different time, like, f64 or something of
their own.
My first solution was to turn the spline type to Spline<T, V>. That would allow people to use both
f32 and f64 as sampling type. The problem kicks in with the code handling those sampling values.
For instance, for the Interpolation::Cosine interpolation mode, I need to take the cosine of a
value which type is either f32 or f64. How can we do that in a generic and polymorphic maneer?
num-traits to the rescue!
Yeah, so was my first thoughts. I then re-wrote most of the interpolating code using num-traits…
and then decided to tackle all the feature gates. Because, I haven’t told you yet, but splines has
several feature gates allowing you to use, e.g., implement the Interpolate trait for some famous
crates’ types (nalgebra, cgmath), enable serde serialization, etc. One feature gate that is
important to me as a demoscener is the "std" feature gate, that, if not defined, makes the
splines crate compile with the no_std feature.
And here comes the first problems. The num-traits has a trait that isn’t compatible with no_std,
the Float trait. I then sat
in front of my computer and thought. I came
to the realization that whatever library I would use for that trait, I would always get my hands
tied by the contracts of the public interface of that crate, for a feature that is almost central
to the whole splines crate. It quickly became obvious that I couln’t use num-traits.
Yes, I know about the
FloatCoretrait. However, I was stuck withFloatConstright after that and I just wanted to clean the interface.
The monomorphic version of splines is easy to implement for no_std, but now I’m stuck because of
some simple traits? Naaah.
So I decided to write my own traits. Enter: Additive, a simple trait with no content but Add and
Sub as supertraits (and some non-important ones for comprehension here), One, a trait that
provides the neutral element of the multiplication for a given type (i.e. the multiplication monoid)
and the Linear<T> trait, providing linear combinations, mandatory for all kind of interpolations
mentioned earlier.
Those traits have universal implementors so that you don’t have to implement them. And enabling a feature gate for, e.g., cgmath will also implement those traits accordingly in an enough polymorphic way that you shouldn’t have to ever worry about them.
Just a little remark of mine: implementing the support for cgmath was pretty simple and
straight-forward. As feared (because I used this crate), nalgebra was way harder to get right —
and I even decided not to implement the Point<..> type because I’ve been struggling all the
afternoon with error types that reminded me old and dark times with C++ Boost library. I’ve already
discussed that with people from the nalgebra world and none seemed to agree with me that this
crate is a little bit too overengineered. For instance, adding the support for nalgebra in
splines also adds several mandatory dependencies — i.e. num-traits and alga. It’s not that bad,
but if you want to play with simple vector spaces, I really do not recommend nalgebra as it’s a
really convoluted library with lots of type aliases and types with infinite counts of type
variables. Maybe it’s just a confirmation bias of mine, because I’ve always used (and written, back
then in C++) linear libraries that were both simple and fast. nalgebra makes me think of this and
this is driving me crazy.
I’m not saying it’s bad. I’m saying it’s not for me and that I wouldn’t recommend it for people
wanting to do simple things — and yes, video game still falls in that simple things bag.
So, the design of feature gating in splines. I know it might feel a bit weird to have gates like
"impl-nalgebra" or "impl-cgmath" but to me it makes lots of sense. For a simple reason: a crate
exposes several types and functions via its public API. Everything that is not public is not for a
very good reason. Mostly, invariants. An invariant is something that must remain true before a
function / morphism / whatever is called and after. What happens in between can actually break
that invariant. The idea is that “If a function breaks internally an invariant, it must restore it
before returning”, so that the API remains safe to use.
Invariants are central in my way of thinking. Everytime I write some code, everytime I design an interaction between several piece of algorithms, crates or even cross-language and cross-systems, I always asks myself “Am I breaking an invariant here? Is it possible to fuck up a state or a hidden property somewhere?”
In Rust, invariants are not a first-class citizen of the language (have a look at how the D
language handles invariant: it’s interesting!). However, they still exist. Imagine this piece of
code:
pub struct Even(pub u64);
impl Even {
pub fn new(nth: u64) -> Self {
Even(nth * 2)
}
}
impl Add<u64, Output = Even> for Even {
fn add(self, rhs: u64) -> Output {
Even(self.0 + rhs * 2)
}
}
Imagine this is wrapped in a crate num-even. Adding a u64 to an Even gives you the nth next
even number. All of this is cool. But there is an invariant. The carried u64 by Even must…
remain even. If at some time a user handle a Even with an odd u64 inside of it, something has
gone wrong terribly. And with this current implementation, it’s very easy to break such an
invariant. Consider:
use num_even::Even;
fn main() {
let mut a = Even::new(1); // the 1st even number
a.0 += 1; // oooooooh, fuuuuuuu-
}
I had long heated debates with people on IRC about why this kind of library should be patched. The main argument of people who would think it shouldn’t be patched is “Yeah but we want users to have access to the underlying objects in any way they want.” I think invariants matter most. The patch version is actually a negative diff:
pub struct Even(u64);
Done. Here, the invariant cannot be broken anymore because people cannot create or modify Even
by hand. Constraining is powerful. You should try it. :)
splines holds keys in a Spline<..> that must remain sorted. Hence, you don’t have a direct
access to the keys nor you can mutate them. Mutating keys would imply resorting the keys in a
smart way, which is something I haven’t needed yet.
So, that’s all for me for today. If you have any question about splines, please feel free to open an issue on GitHub. Also, if you’re interested in trying it, please have a look at the splines-1.0.0-rc.1 release candidate. It contains everything you need to get started!
The documentation is not up to date and some is missing, but it should be enough to start.
As always, keep the vibes!
]]>Well, I’ve changed my mind. Some people pointed out that the good thing to do for most GPU is to align on 32-bit. That is, 4 bytes. The alignment should be 4 bytes, then, not 1.
There might be an issue with that. If you store a structure with attributes which sizes are not a multiple of 4 bytes, it’s likely you need to add padding.
However, I just reviewed my code, and found this:
instance (GPU a,KnownNat n,Storable a) => Vertex (V n a) where
instance (Vertex a,Vertex b) => Vertex (a :. b) where
Those are the single instances for Vertex. That means you can only use V
and (:.) to build up vertices. Look at the V instance. You’ll find a GPU
typeclass constraint. Let’s look at its definition and instances:
class GPU a where
glType :: Proxy a -> GLenum
instance GPU Float where
glType _ = GL_FLOAT
instance GPU Int32 where
glType _ = GL_INT
instance GPU Word32 where
glType _ = GL_UNSIGNED_INT
Woah. How did I forget that?! Let me translate those information to you. That means we can only have 32-bit vertex component! So the memory inside vertex buffers will always be aligned on 4 bytes! No need to worry about padding then!
The first implication is the fact you won’t be able to use Word16, for
instance. You’ll need to stick to the three types that have a GPU instance.
Note: that doesn’t prevent us from adding Double later on, because a
Double is a 64-bit type, which is a multiple of 4 bytes!
That’s all I have for today. I’m working on something very exciting linked to render batching. I’ll talk about that when it’s cooked. ;)
Keep the vibe; keep building awesome things, and as always, thank you for reading me!
]]>luminance-0.1 was released yesterday night, along with luminance-samples-0.1! I’ll need to enhance the documentation and add directions so that people don’t feel too overwhelmed.
I’m also going to write a wiki to help people get their mind wrapped around luminance.
If you think something is missing; if you think something could be enhanced; or if you’ve found a bug, please, feel free to fill in an issue on the issues tracker.
I need to test the framework. I need a lot of tests. I’ll write a demoscene production with it so that I can give a good feedback to the community and prove that luminance can be used and works.
In the waiting, keep the vibe!
]]>It’s been a while I haven’t released anything on my blog. I just wrote a few changes for the latest version of luminance, luminance-0.8.2 and I decided to write about it because I think those changes are interesting on a Haskell level.
If you haven’t read the changelog
yet, I changed the createProgram function and the way it handles uniform interfaces. In
luminance < 0.8, you were provided with as many functions as there are uniform kinds. Up to now,
luminance supports two uniform kinds:
So you had two rank-2 functions like forall a. (Uniform a) => Either String Natural -> UniformInterface m (U a) and forall a. (UniformBlock a) => String -> UniformInterface m (U (Region rw (UB a))) to map whichever uniforms you wanted to.
The issue with that is that it requires to break the interface of createProgram each time we want
to add a new kind of uniform, and it’s also a pretty hard to read function signature!
So… how does luminance-0.8 solve that?
What is the only way we have to select uniforms? Names. Names can either be a String or a
Natural for explicit semantics. We could encode such a name using an algebraic data type:
data UniformName
= UniformName String
| UniformSemantic Natural
deriving (Eq,Show)
That’s a good start. Though, we still have the problem of choosing the kind of uniform because we
still have several functions – one per kind. We could encode the kind of the uniform directly into
the name. After all, when we ask for a uniform mapping through a name, we require to know the kind.
So that kind of makes sense. Let’s change our UniformName type:
data UniformName :: * -> * where
UniformName :: String -> UniformName a
UniformSemantic :: Natural -> UniformName a
UniformBlockName :: String -> UniformName (Region rw (UB a))
That’s neat, but with that definition, we won’t go anywhere, because we’re too polymorphic. Indeed,
UniformName "foo" :: UniformName a can have any a. We need to put constraints on a. And that’s
where GADTs come in so handy! We can hide the constraints in the constructors and bring them into
scope when pattern matching. That’s a very neat feature of GADTs. So now, let’s add some
constraints to our constructors:
data UniformName :: * -> * where
UniformName :: (Uniform a) => String -> UniformName a
UniformSemantic :: (Uniform a) => Natural -> UniformName a
UniformBlockName :: (UniformBlock a) => String -> UniformName (Region rw (UB a))
Yes! Now, we can write a function that takes a UniformName a, pattern matches it and call the
appropriate function regarding the infered shape of a!
However, how do we forward the error? In older version of luminance, we were using ProgramError
and more especially two of its constructors: InactiveUniform and InactiveUniformBlock. We
need to shrink that to a single InactiveUniform constructor and find a way to store our
UniformName… But we can’t yet, because of the a parameter! So the idea is to hide it through
existential quantification!
data SomeUniformName = forall a. SomeUniformName (UniformName a)
instance Eq SomeUniformName where
-- …
instance Show SomeUniformName where
-- …
And now we can store SomeUniformName in InactiveUniform. We won’t need to recover the type, we
just need the constructor and the carried name. By pattern matching, we can recover both those
information!
Feel free to have a look at the new createProgram function.
As you will see, the type signature is easier to read and to work with! :)
Have fun, and keep the vibe!
]]>Shaders are very common units in the world of graphics. Even though we’re used to using them for shading1 purposes, they’re not limited to that. Vulgarisation has ripped off the meaning up and down so much that nowadays, a shader might have nothing related to shading. If you’re already doing some graphics, you may know OpenGL and its compute shaders. They have nothing to do with shading.
You might also already know shader toy. That’s a great place to host cool and fancy OpenGL shaders2. You write your shaders in GLSL3 then a GLSL compiler is invoked, and your shader is running on the GPU.
So you write your shader as a source code in a host language, for instance in C/C++, Java, Haskell, whatever, and you end up with a shader running on GPU.
There’re two nasty issues with that way of doing though:
They’re both serious issues I’m going to explain further.
This is problematic for a good reason: a lot of shaders are application dependent. Shadertoy is a nice exception, just like modeling tools or material editors, but seriously, in most applications, end users are not asked the shaders to run with. In a game for instance, you write all the shaders while writing the game, and then release the whole package.
Yeah… What’s about additional content? Per-map shaders, or that kind of stuff?
Those shaders are like resources. That doesn’t imply using them as is though. We could use dynamic relocatable objects (.so or .dll) for instance.
What compile-time compilation gives you?
It gives you something hyper cool: host language features. If you have a strongly-typed language, you’ll benefit from that. And that’s a huge benefit you can’t get away from. If you’re writing an incorrectly typed shader, your application / library won’t compile, so that the application won’t react in weird way at run-time. That’s pretty badass.
This issue is not as important as the first one, but still. If you’re working on a project and you target several platforms (among ones using OpenGL, OpenGL ES, DirectX and a soft renderer), you’ll have to learn several shading languages as well (GLSL, HLSL4).
In order to solve that, there’re two ways to go:
A DSL is appealing. You have a standalone language for writing shaders, and backends for a compiler/language. However, that sounds a bit overwhelming for such an aim.
An EDSL is pretty cool as well. Take a host language (we’ll be using Haskell) and provide structure and construction idioms borrowed from such a language to create a small embedded one. That is the solution I’m going to introduce.
Ash stands for Abstract Shader. It’s a Haskell package I’ve been working on for a few weeks now. The main idea is:
I guessed it’d be a good idea to share my thoughts about the whole concept, since I reckon several people will be interested in such a topic. However, keep in mind that Ash is still a big work in progress. I’m gonna use several blog entries to write down my thoughts, share it with you, possibly enhance Ash, and finally release a decent and powerful library.
If you’re curious, you can find Ash here.
Ash is a library that provides useful tools to build up shaders in Haskell. In Ash, a shader is commonly function. For instance, a vertex shader is a function that folds vertex components down to other ones – possibly maps, but it could add/remove components as well – and yields extra values for the next stages to work with.
You write a shader with the Ash EDSL then you pass it along to a backend compiler.
Here are two examples. In order for you to understand how Ash works, I’ll first write the GLSL (330 core) shader, then the Ash one.
Let’s write a vertex shader that takes a position and a color, and projects the vertex using a perspective matrix, a view matrix and the object matrix of the object currently being rendered and passes the color to the next stage:
#version 330 core
in vec3 pos;
in vec4 col;
out vec4 vcol;
uniform mat4 projViewModel;
void main() {
vcol = col;
gl_Position = projViewModel * vec4(pos, 1.);
}
And now, the Ash one:
vertexShader :: Ash (M44 Float -> V3 Float :. V4 Float -> V4 Float :. V4 Float)
vertexShader = lam $ \proj -> lam $ \v ->
let pos :. col = v
in proj #* v3v4 pos 1 :. col
Ash is the type used to lift the shading expression up to Haskell. You use it to use the EDSL. It actually represents some kind of HOAST7.
Then, you can find M44, V3, V4 and (:.).
M44 is the type of 4x4 matrices. Since projection matrix, view matrix and model matrix are all 4x4 floating matrix, M44 Float makes sense.
V3 and V4 represents 3D and 4D vectors, respectively. V3 Int is three ints packed in a vector as well as V4 Float is four floats packed in a vector. You’ll also meet V2, which is… the 2D version.
(:.) is a type operator used to build tuples. You can see (:.) as a generalized (,) – the default Haskell pair type – but (:.) is more power full since it can flatten expressions:
a :. (b :. c) = a :. b :. c
The (:.) has a lot of uses in Ash. In our cases, a chain of (:.) represents a vertex’ components.
So our vertexShader value is just a function that takes a matrix and a vertex (two components) and outputs two values: the new position of the shader, and the color. Let’s see the body of the function.
lam $ \proj -> lam $ \v ->
This is a pretty weird expression, but I haven’t found – yet? – a better way to go. lam is a combinator used to introduce lambdas in the EDSL. This expression then introduces a lambda that takes two values: proj and v. You can read that as:
\proj v ->
Next:
let pos :. col = v
This is the tricky part. That let expression extracts the components out of the vertex and binds them to pos and col for later use.
in proj #* v3v4 pos 1 :. col
(#*) is a cool operator used to multiply a matrix by a vector, yielding a new vector.
(v3v4) is a shortcut used to to build a V4 using a V3 by providing the missing value – here, 1. You’ll find similar functions, like v2v3 and v2v4, to respectively build a V3 from a V2 by providing the missing value and build a V4 from a V2 by providing the two missing values.
We finally wrap the result in a tuple (:.), and we’re done.
Ash embeds regular linear expressions (vectors, matrix), textures manipulation, tuples creation, let-bindings, lambda functions (they represent shader stages up to now), and a lot of other features.
Each feature is supposed to have an implementation in a given backend. For instance, in the GLSL backend, a lambda function is often turned into the well done main function. Its parameters are expanded to as in values, and
control parameters are uniform variables.
Each backend is supposed to export a compile function – the name may varies though. However, each backend is free to compiles to whatever smart they think is. For instance, compiling an Ash shader to GLuint (shader stage) is not very smart since it would use IO and handles error a specific way we don’t want it to do. So the GLSL compiler is a function like glslCompile :: Ash … -> Either CompilationError String, and the String can be used as a regular GLSL source code string you’ll pass to whatever implementation of shader you’ve written.
I need to finish the implentation of the EDSL, and write the whole GLSL 330 compiler. If it’s a success, I’ll accept pull-requests for other famous compilers (other GLSL version compilers, HLSL, and so on and so forth).
Once that done, I’ll write a few other blog entries with example as a proof-of-concept :)
Shading is the process in which primitives (sets of vertices) are turned into colors (i.e fragments, a.k.a. pixels or texels).
Actually, they’re fragment shaders.
Embedded Specific Language.
High-Order Abstract Syntax tree; for the purpose of this paper, you don’t have to fully understand them to get your feet wet with Ash (which is cool, right? :) ).
Soooooooooooo. Because glsl is written with nom in its third iteration and because nom is now
at version 4, I decided it was time to update the parser code of glsl. I heard about the
comparison between pest and nom and decided to write an implementation with pest.
This is the first article of a series about how writing a pest looks like is fun compared to writing the nom parser. I’ll post several articles as I advance and see interesting matter to discuss and share.
First thing first: pest consumes a file at compile-time that contains a PEG grammar defining the format of input you want to parse. PEG – which stands for Parsing Expression Grammar – is a formal language that enables you to describe your own language with rules. Those rules are written with some basic blocks that belong to Language Theory – if you’ve ever heard of Stephen Kleene and its famous Kleene star for instance, you will feel familiar with PEG.
What I love about PEG is that with a very limited set of constructs, you can describe a lot of determinist languages. In the case of GLSL450 – which is the language that the glsl crate can parse – it’s a context-free and determinist language. So the whole language can be defined in terms of (recursive) PEG rules.
The PEG primitive constructs available in pest are:
"foo": this rule will recognize any string that is "foo".'a' .. 'z': this will recognize any char in the range,
like "a", "d" or "z".a ~ b: this rule will be matched if it can match the a
rule followed by the b rule; "foo" ~ 'a' .. 'z' will recognize "foot", "food", "fooz"
but also "foo r" – this whitespace behavior is explained below – etc.a | b: this rule will match either a
if it succeeds or b if a fails and b succeeds or none otherwise. "foo" | 'a' .. 'z' will
be matched with "foo" and "a" or "f".? suffix operator, like a?: this will match a if it succeeds or
will just ignore its failure if it fails, making it optional. For instance, "hello"? ~ "world"
will match both "hello world" and "world".(a ~ b).a*: this rule matches zero or more times the
a rule. For instance, ('0' .. '9')* will match "0" and "314" but will also match "".a+: this rule matches one or more times the
a rule. For instance, ('0' .. '9')+ will match "0" and "314" but will not match ""
because it expects at least one digit.{min, max} notation. The rule a{0,} is akin to a*: it must be matched at least 0
times and at maximum infinite times. The rule a{,3} must be matched between 0 and 3
times and the rule a{1, 12} must be matched between 1 and 12 times.Additionally, pest provides a notation to add rule modifiers:
rule_name { definition }, like number = { ASCII_DIGIT+ }.~, * and + rules get interleaved with two special rules if defined:
WHITESPACE, defining how to eat a single whitespace.COMMENT, defining how to eat a comment.(WHITESPACE | COMMENT)* so that number = { ASCII_DIGIT+ }
gets actually rewritten as number = { (ASCII_DIGIT | (WHITESPACE | COMMENT)*)+ }.number
rule defined above, both "324" and "3 2 4" will get matched if we defined WHITESPACE to
eat " ". In order to fix this, the @ modifier can be prepended to the rule definition:
number = @{ ASCII_DIGIT+ }@ for @tomic. :)_ modifier can be used to mute a rule. For instance, eating whitespaces will not provide
you with any useful tokens. That’s why the WHITESPACE rule is defined as
WHITESPACE = _{ " " }.$ and ! but we’re not interested in them as they’re
very specific.So, basically, you write a (long) list of PEG rules in a file, give that file to Rust via a proc-macro derive, et voila!
#[derive(Parser)]
#[grammar = "path/to/grammar.pest"]
struct Parser;
It’s that simple.
PEG is really cool to use… but there are caveats. For instance, PEG / pest cannot express left-recursive rules. To understand this, take this rule:
foo = { a | b ~ foo* }
This rule is correct and will allow you to match against "a" or "b" or even "bbbbbbbbb" and
"bbbbbbbba". However, the following one is left-recursive and won’t be able to be compiled:
foo = { a | foo* ~ b }
As you can see, the second alternative is left-recursive. Imagine matching against "a": the first
alternative fires, everything is great. Now match against "b". The first alternative fails so
pest tries the second one, but in order to take the second one, it needs to try at least once
foo (the foo*) to see whether it fails or not. If it fails, it will then ignore and try to eat a
"b". If it succeeds, it will try to fire the foo rule again. However, when it tries foo for
the first time, it will try the first alternative again, a, then will fail, then will try foo*,
then a, then foo*, then… You see the pattern.
Left-recursive rules are then forbidden so that we can escape that situation. PEGs need to be unambiguous and the rules (especially alternatives) are tested in order. You will have to unroll and optimize those rules by hand. In our case, we can rewrite that rule in a PEG-compliant way:
foo = { (a | b)+ }
An issue I had while writing the PEG of GLSL450 was with the expression rule (among a loooot of
others). Khronos’ specifications are written with left-recursive rules pretty much everywhere and
this is the one for expressions:
expression:
assignment_expression
expression COMMA assignment_expression
Read it as “An expression is either an assignment expression or an expression with a comma followed
by an assignment expression”. This actually means that the second alternative cannot be constructed
without the first one – because it’s recursive! So you must start with an assignment_expression.
Then you can have the second rule applied recursively, like so (parenthesis just for convenience to
show the left-recursion):
((((assignment_expression) COMMA assignment_expression) COMMA assignment_expression) COMMA assignment_expression)
Etc., etc. If you remove the parenthesis, you get:
assignment_expression COMMA assignment_expression COMMA assignment_expression COMMA assignment_expression
Which you should now be able to write with a PEG rule easily! Like so:
expression = { assignment_expression ~ (COMMA ~ assignment_expression)* }
Way better and without left-recursion! :) So I spent pretty much a whole afternoon trying to remove left-recursions (and some rules were really nasty). I even have one remaining rule I’ll have to cut like a surgeon later to make it fully available (I had to do the same thing with nom; it’s the rule allowing postfix expressions to be used as function identifiers; issue here).
Then I started to play with those generated Rule types… wait, what?! The generated Rule type?
Ok so that was my first surprise: the grammar knows how the rules are (deeply) nested but pest
outputs a single Rule type with all the rules laid down as flat variants for that Rule! This was
at first very confusing for me as I thought I’d be visiting freely through the AST – because this
is what a grammar actually defines. Instead, you must ask your Parser type which rule you want
to match against. You get a Pairs, which is an iterator over Pairs.
A Pair defines a single token that is defined by its starting point and ending point (hence a
pair… yeah I know, it’s confusing). A Pair has some methods you can call on it, like as_str() to
retrieve its string slice in the (borrowed) input of your parser. Or into_inner(), giving you a
Pairs that represent the matched inner tokens, allowing you to eventually visit through the tree
of tokens.
I have two problems with that.
First, because pest doesn’t fully use the grammar information at type-level, you lose the one
crucial information: the actual topology of your grammar in the type system. That means that every
time you will ask for a given Pair, you will have to call as_rule() on it an pattern-match on
the whole list of variants that compose your grammar! In all situations, you will only need to
match against a very few of them and will discard every others. And how do you discard them? –
you know those are impossible because the very definition of the grammar? You use
unreachable!(), which I find almost as unsafe and ugly as using unimplemented!(). I know
unreachable!() is required in certain situations, but having it spawn in a parser using the public
API of pest makes me gnash my teeth. Example of code I’m currently writing:
fn parse_type_specifier_non_array<'a>(pair: Pair<'a>) -> Result<syntax::TypeSpecifierNonArray, String> {
let inner = pair.into_inner().next().unwrap();
match inner.as_rule() {
Rule::struct_specifier => {
// …
}
Rule::identifier => {
let identifier = inner.as_str();
match identifier {
"void" => Ok(syntax::TypeSpecifierNonArray::Void),
"float" => Ok(syntax::TypeSpecifierNonArray::Float),
"double" => Ok(syntax::TypeSpecifierNonArray::Double),
// …
_ => Ok(syntax::TypeSpecifierNonArary::TypeName(identifier.to_owned())
}
}
_ => unreachable!()
}
}
And I reckon I will find that pattern a lot in my next few hours / days of writing those functions.
That parse_type_specifier_non_array function of mine also pinpointed another issue I had: initially,
I wrote that logic in the type_specifier_non_array rule in the PEG grammar, like:
type_specifier_non_array = {
"void" |
"float" |
// …
identifier
}
And quickly came to the realization that the identifier alternative would get muted very often
by the lexemes alternatives. Imagine two strings, "int x = 3;" and "interop x = …;". The first
type will get the right TypeSpecifierNonArray::Int AST representation and the second string… will
get the same because the "int" rule will fire first, leaving some unmatched text without any
sense ("erop x = …;").
What that means is that we cannot express this rule this way with PEG. I’ve been told pest should
now support the ! operator that can be used to encode something that must not be parsed. But
that would just slightly hide the problem: there is no way in the PEG rules to actually do parsing
transformation. What pest really is here is a lexer: it generates tokens. Yes, it does place
them in a tree (token tree), but they all remains tokens (strings). And that is when I thought about
the difference between pest and nom
nom is a parser combinator. That means you can build bigger parsers by combining small parsers.
The correct term to talk about nom is that it’s a scannerless parser: it doesn’t require to
generate tokens prior to do parsing and prefers to do both at the same time. nom parsers are
typically implemented using macros like preceded!, delimited!, take_until!, tag!, value!
and do_parse!, allowing for matching (lexing) slices of bytes / chars and parsing them to actual
values with the type of your choice.
However, pest relies on a PEG file, representing the formal grammar of a language to tokenize.
That lexer phase takes place and has to be able to tokenize the whole input before giving hand back.
I’m not sure when I say this (but I’m pretty convince it’s the case): pest doesn’t support
streaming input, since it needs to eat the special rule EOI – End Of Input – or eat a rule error
(to succeed with the previous rule or propagate the error upwards) before returning. nom can be
used to eat streams of bytes, though.
And then, you have two choices with pest:
I’m currently experimenting with (2.). The type_specifier_non_array rule is one of the biggest one
in my grammar so maybe I’ve already done the worst case but I’m fearing I’ll have long nights of
coding before ending up with a benchmark pest vs. nom.
In the meantime:
master.
Issue here.#define and other preprocessors pragmas. I’m taking
contributions (implementations, ideas) to fix that problem.
Issue here.syntax module (the AST types) don’t have new methods. Those
methods are important to me as they enable to bypass some limitation from the current
glsl-quasiquote implementation that doesn’t allow variable interpolation.Thanks for having read through, and keep the vibes!
]]>The latest version of luminance has one of the two features. UBO were added and SSBO will follow for the next version, I guess.
UBO stands for Uniform Bbuffer Object. Basically, it enables you to create uniform blocks in GLSL in feed them with buffers. Instead of passing values directly to the uniform interface, you just write whatever values you want to to buffers, and then pass the buffer as a source for the uniform block.
Such a technique has a lot of advantages. Among them, you can pass a lot of values. It’s also cool when you want to pass values instances of a structure (in the GLSL source code). You can also use them to share uniforms between several shader programs as well as quickly change all the uniforms to use.
In luminance, you need several things. First thing first, you need… a buffer! More specifically,
you need a buffer Region
to store values in. However, you cannot use any kind of region. You have to use a region that can
hold values that will be fetched from shaders. This is done with a type called UB a. A buffer of
UB a can be used as UBO.
Let’s say you want to store colors in a buffer, so that you can use them in your fragment shader. We’ll want three colors to shade a triangle. We need to create the buffer and get the region:
colorBuffer :: Region RW (UB (V3 Float)) <- createBuffer (newRegion 3)
The explicit type is there so that GHC can infer the correct types for the Region. As you can see,
nothing fancy, except that we just don’t want a Region RW (V3 Float but
Region RW (UB (V3 Float)). Why RW?
Then, we’ll want to store colors in the buffer. Easy peasy:
writeWhole colorBuffer (map UB colors)
colors :: [V3 Float]
colors = [V3 1 0 0,V3 0 1 0,V3 0 0 1] -- red, green, blue
At this point, colorBuffer represents a GPU buffer that holds three colors: red, green and blue.
The next part is to get the uniform interface. That part is experimental in terms of exposed
interface, but the core idea will remain the same. You’re given a function to build UBO uniforms
as you also have a function to build simple and plain uniforms in
createProgram:
createProgram shaderList $ \uni uniBlock -> {- … -}
Don’t spend too much time reading the signature of that function. You just have to know that
uni is a function that takes Either String Natural – either a uniform’s name or its integral
semantic – and gives you mapped U in return and that uniBlock does the same thing, but for
uniform blocks instead.
Here’s our vertex shader:
in vec2 co;
out vec4 vertexColor;
// This is the uniform block, called "Colors" and storing three colors
// as an array of three vec3 (RGB).
uniform Colors {
vec3 colors[3];
};
void main() {
gl_Position = vec4(co, 0., 1.);
vertexColor = vec4(colors[gl_VertexID], 1.);
}"
So we want to get a U a mapped to that "Colors" uniform block. Easy!
(program,colorsU) <- createProgram shaderStages $ \_ uniBlock -> uniBlock "Colors"
And that’s all! The type of colorsU is U (Region rw (UB (V3 Float))). You can then gather
colorBuffer and colorsU in a uniform interface to send colorBuffer to colorsU!
You can find the complete sample here.
Finally, you can augment the type you can use UB with by implementing the UniformBlock
typeclass. You can derive the Generic typeclass and then use a default instance:
data MyType = {- … -} deriving (Generic)
instance UniformBlock MyTpe -- we’re good to go with buffer of MyType!
I added luminance and luminance-samples into Stackage. You can then find them in the nightly snapshots and the future LTS ones.
I plan to add stencil support for the framebuffer, because it’s missing and people might like it included. I will of course add support for SSBO* as soon as I can. I also need to work on cheddar but that project is complex and I’m still stuck with design decisions.
Thanks for reading my and for your feedback. Have you great week!
]]>The way it typically works is by writing the GLSL code in a hard-coded string, or a file that is dynamically loaded at
runtime by your application (some people even pre-compile GLSL into a binary form, but it doesn’t change the fact that
the binary has to be brought to the GPU at runtime). The overall implication of all this is that the shader is
represented via an opaque String (or binary like Vec<u8>) in your application.
A couple of examples of interfaces to handle shaders, using wgpu and a crate of mine, luminance:
As you can see, dealing with shader sources implies a string interface, because that’s basically code that needs to be compiled by your GPU driver (done at runtime). That has several issues:
String duplication all over the place.String doesn’t allow you to be sure that shader is compatible with the graphics pipeline.
Being the author of luminance, that is a massive flaw to me. As you might know, the typing in luminance is pretty
advanced (phantom typing, type families, type states, etc.), so being stuck with String is not a good situation.shadesI introduced shades a couple of years ago without going public about it. The reason for that was that it was not
completely ready for people to use. Of course you could have given a try (and fundamentally, it hasn’t changed much),
but the syntax was going to drive people off right away. The idea of shades is to write shaders directly in Rust.
Instead of using String to represent a shader stage, you use Stage<Inputs, Outputs, Environment>. That gives you a
first visible advantage: typing. In shades, you don’t have to declare your inputs, outputs nor environment. For
instance, compare the following GLSL code with its shades counterpart:
uniform float time;
uniform vec2 resolution;
Whenever a shader stage has to use time or resolution, they will have to declare those two lines at the top of their
sources. With shades:
#[derive(Debug)]
struct Env {
time: f32,
resolution: V2<f32>,
}
#[derive(Debug)]
struct EnvExpr {
time: Expr<f32>,
resolution: Expr<V2<f32>>,
}
impl Environment for Env {
type Env = EnvExpr;
fn env() -> Self::Env {
EnvExpr {
time: Expr::new_env(0),
resolution: Expr::new_env(1),
}
}
fn env_set() -> Vec<(u16, Type)> {
vec![
(0, <f32 as ToType>::to_type()),
(1, <V2<f32> as ToType>::to_type()),
]
}
}
When using a Stage<_, _, Env>, time and resolution will automatically be available, whenever you use them or not.
This is even more interesting with procedural macro support in luminance for instance:
// the Environment and Semantics derive macro will generate all the above code for you, as well as the type family
#[derive(Debug, Environment, Semantics)]
struct Env {
time: f32,
resolution: V2<f32>,
}
So yes, shades is a bit more verbose at the Rust level, but I think it’s worth it because shaders are checked at
compile-time (via rustc) and not at runtime anymore. Typing also allows a better composition and reusability.
Just to give you an idea, this is an old and outdated example taken from the repository to show you what it used to be:
let vertex_shader = StageBuilder::new_vertex_shader(
|mut shader: StageBuilder<MyVertex, (), ()>, input, output| {
let increment = shader.fun(|_: &mut Scope<Expr<f32>>, a: Expr<f32>| a + lit!(1.));
shader.fun(|_: &mut Scope<()>, _: Expr<[[V2<f32>; 2]; 15]>| ());
shader.main_fun(|s: &mut Scope<()>| {
let x = s.var(1.);
let _ = s.var([1, 2]);
s.set(output.clip_distance.at(0), increment(x.clone()));
s.set(
&output.position,
vec4!(input.pos, 1.) * lit![0., 0.1, 1., -1.],
);
s.loop_while(true, |s| {
s.when(x.clone().eq(1.), |s| {
s.loop_break();
s.abort();
});
});
})
},
);
let output = shades::writer::glsl::write_shader_to_str(&vertex_shader).unwrap();
That snippet doesn’t do anything really logical; it’s just there to show you how things look like.
I wrote a couple of things with shades, and even though I like it, I have to admit that the Rust API is pretty hard to read, use and understand. I needed something better.
shades: shades-edslThen it struck me: why would that API need to be the one people use? It can remain public, but maybe we can build
something above it to make it much easier to use. The initial idea of that API was to be an EDSL, but Rust has a lot
of limitations (for instance, Eq methods, Ord methods, etc. return bool, while I would need them to return
Expr<bool>). So… I came to the realization that the right way to do this would be to write a real EDSL: shades-edsl.
The idea of shades-edsl is super simple: remove everything that would make shades usable directly by a human, and build a procedural macro that transforms regular Rust code into the API from shades. Basically, generate the shades code inside the code of a procedural macro, which is what humans will use.
The way it works is pretty straight-forward to understand: you write your shading code in a shades! { … } block.
Everything in that block is reinterpreted and replaced by shades symbols: builders, method calls, etc. etc.
For instance, the following:
let stage = shades! { VS |_input, _output, _env| {
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
// do nothing
}
}};
Will be reinterpreted as:
let stage = shades::stage::StageBuilder::<_, _, _>::new_vertex_shader(…);
Where … is generated code (a lambda here). A couple of more example:
// in a shades! block
if cond {
body_if
} else {
body_else
}
is transformed into:
__scope.when(cond, |__scope| {
#body_if // body_if is also transformed
}).or_else(|__scope| {
#body_else
});
const declarations work and declare constant items in the shading code.fn will create function declarations and return a function handle (so that you
can call the function).main will not return a function handle, but instead will inject the function declaration and
will return the Stage. It is the only way to produce a Stage so you have the certainty that a Stage contains
the main function.shades-edsl is implemented as a procedural macros, shades!, which defines a shader stage. It’s basically a
monadic computation (using shades::stage::StageBuilder behind the scene). Once you write your fn main() function,
the builder simply generates the Stage object for you (using StageBuilder::main_fun). In terms of architecture, it’s
a pretty simple one:
shades! invocation (TokenStream).TokenStreamParsing the Rust code uses syn, a famous parsing crate used a lot by procedural macros. It was made to parse Rust-like
code, but it has some interesting use cases. I’m used to writing procedural macros in luminance-derive for instance
(#[derive()] annotations).
The idea is to take a Rust item and represent it as a data type. The important thing to know with syn is that it supports nice error message reporting, so you are advised to store non-meaningful information, like braces, parens, tokens, etc.
For instance:
let x: Type = 3;
We can represent such an item (a let declaration) with the following struct:
#[derive(Debug)]
pub struct VarDeclItem {
let_token: syn::Token![let],
name: syn::Ident,
ty: Option<(syn::Token![:], syn::Type)>,
assign_token: syn::Token![=],
expr: syn::Expr,
semi_token: syn::Token![;],
}
syn::Token!is a type macro that resolves to the right type name for the given symbol.
Parsing in syn uses the Parse trait, and is more intimidating than hard to use. This is the whole parser for
VarDeclItem:
impl Parse for VarDeclItem {
fn parse(input: syn::parse::ParseStream) -> Result<Self, syn::Error> {
let let_token = input.parse()?;
let name = input.parse()?;
let ty = if input.peek(Token![:]) {
let colon_token = input.parse()?;
let ty = input.parse()?;
Some((colon_token, ty))
} else {
None
};
let assign_token = input.parse()?;
let expr = input.parse()?;
let semi_token = input.parse()?;
Ok(Self {
let_token,
name,
ty,
assign_token,
expr,
semi_token,
})
}
}
ParseStream is the current state of the input stream. As you can see with the type of parse(), it will return the
Self type or an error, so we can call input.parse()? inside the implementation if type inference knows what the type
is, and that the type implements Parse. In our case, syn::Token![let] and syn::Ident do implement Parse, so we
can parse them in two simple lines:
let let_token = input.parse()?;
let name = input.parse()?;
Then, we need to look ahead to check whether there is a semicolon (:). If there is, then we can parse syn::Token![:]
and syn::Type, as both already implement Parse. Otherwise, we simply assume None as type ascription:
let ty = if input.peek(Token![:]) {
let colon_token = input.parse()?;
let ty = input.parse()?;
Some((colon_token, ty))
} else {
None
};
The rest is as simple as everything implements Parse as well:
let assign_token = input.parse()?;
let expr = input.parse()?;
let semi_token = input.parse()?;
We have everything we need to return the parsed declaration:
Ok(Self {
let_token,
name,
ty,
assign_token,
expr,
semi_token,
})
Every Rust items are parsed like that. There are a couple of harder things. For instance, input can be split by parsing matched delimiters, resulting in two disjoint inputs, or some items will have to be delimited with punctuation (a token).
An example of disjoint input parsing while parsing if statements:
#[derive(Debug)]
pub struct IfItem {
if_token: Token![if],
paren_token: Paren,
cond_expr: Expr,
brace_token: Brace,
body: ScopeInstrItems,
else_item: Option<ElseItem>,
}
// …
impl Parse for IfItem {
fn parse(input: syn::parse::ParseStream) -> Result<Self, syn::Error> {
let if_token = input.parse()?;
let cond_input;
let paren_token = parenthesized!(cond_input in input);
let cond_expr = cond_input.parse()?;
let body_input;
let brace_token = braced!(body_input in input);
let body = body_input.parse()?;
let else_item = if input.peek(Token![else]) {
Some(input.parse()?)
} else {
None
};
Ok(Self {
if_token,
paren_token,
cond_expr,
brace_token,
body,
else_item,
})
}
}
Here, we use the parenthesized! macro that parses a matching pair of (). Once it has finished, the input has
advanced after the closing delimiter, but the second input (cond_input in our case) is scoped right after the opening
delimiter and right before the closing one. That allows us to parse “inside” the parenthesis, which is a weird interface
at first, but once you get used to it, it’s actually pretty smart and pretty fast. Notice how we parse the conditional
expression using cond_input and not input here:
let cond_input;
let paren_token = parenthesized!(cond_input in input);
let cond_expr = cond_input.parse()?;
Same thing with {} and the braced! macro:
let body_input;
let brace_token = braced!(body_input in input);
let body = body_input.parse()?;
An even more interesting example: parsing function definitions:
#[derive(Debug)]
pub struct FunDefItem {
fn_token: Token![fn],
name: Ident,
paren_token: Paren,
args: Punctuated<FnArgItem, Token![,]>,
ret_ty: Option<(Token![->], Type)>,
brace_token: Brace,
body: ScopeInstrItems,
}
Here, args is a punctuated sequence of FnArgItem, delimited with ,. The parser is as follows:
impl Parse for FunDefItem {
fn parse(input: syn::parse::ParseStream) -> Result<Self, syn::Error> {
let fn_token = input.parse()?;
let name = input.parse()?;
let args_input;
let paren_token = parenthesized!(args_input in input);
let args = Punctuated::parse_terminated(&args_input)?;
let ret_ty = if input.peek(Token![->]) {
let arrow_token = input.parse()?;
let ret_ty = input.parse()?;
Some((arrow_token, ret_ty))
} else {
None
};
let body_input;
let brace_token = braced!(body_input in input);
let body = body_input.parse()?;
Ok(Self {
fn_token,
name,
paren_token,
args,
ret_ty,
brace_token,
body,
})
}
}
The argument part uses parenthesized! to parse the all arguments definitions, and then, with a single argument parser
(identifier plus type: ident: Type), we can simply ask to parse many by using comma punctuation. This is done with
parse_terminated:
let args_input;
let paren_token = parenthesized!(args_input in input);
let args = Punctuated::parse_terminated(&args_input)?;
Parsing with syn is a bit weird at first (I’m used to nom because of glsl), but in the end, I think it’s a pretty funny and interesting interface. There is no combinator such as alternative parser selection, but that’s probably for the best, because the peek / look ahead interface forces you to write fast parsers. I really like it.
Mutating the AST is a top-down operation that consists in transforming types and expressions. The goal is to lift items up the shades EDSL. A function such as:
fn add(a: i32, b: i32) -> i32
Needs to be representable in the shades AST. For this reason, a, b and the return type have to change types.
Changing the type implies going from T to Expr<T>. That operation is done in a mutate(&mut self) method on each
syntax items parsed from the previous stage, and each of mutate invocation might call mutate on items they contain
(such as punctuated items, collections of instructions / statements, etc.).
The only interesting part to discuss here is probably how we can lift values in complex expressions such as
a + (b * c). This is done with the concept of mutable visitors, defined in syn with the visit-mut feature gate.
A mutable visitor is simply an object that implements a trait (VisitMut), which defines one method for each type of
items in the AST that might exist. For instance, there is visit_expr_mut(&mut self, e: &mut syn::Expr) and a
visit_type_mut(&mut self, e: &mut syn::Type). All visit_* methods have a default implementation doing nothing, so
that you are left with implementing only the ones that matter for you.
An example of what I do with types:
struct ExprVisitor;
impl VisitMut for ExprVisitor {
fn visit_type_mut(&mut self, ty: &mut Type) {
*ty = parse_quote! { shades::expr::Expr<#ty> };
}
}
parse_quote!is a syn macro that is akin toquote!from quote, but can infer the return type to parse to the right type. Here, the returned type is inferred to besyn::Type.
An interesting thing with shades-edsl: because users will provide their inputs, outputs and environment types in the
types of Stage, we do not want to wrap those in Expr, because they already contain Expr values, as seen earlier.
For this reason, the syntax of types of arguments was augmented with the possibility to add a dash (#) in behind the
types. If such a symbol is found, the type is assumed unlifted and shouldn’t be lift. For instance:
let a: i32 = …; // will become Expr<i32>
let b: #Foo = …; // will remain Foo
shades codeGenerating the shades AST is just a matter of traversing the parsed and mutated AST and using the quote crate and
its quote! macro to write the Rust code we want. The only missing part is how to bootstrap a StageBuilder, which is
done with some more parsing. Invocation looks like this:
let stage = shades! { vertex |input: TestInput, output: TestOutput, env: TestEnv| {
The first keyword is vertex here, but it could be fragment, geometry or any kind of shader stage (that needs to be
documented). Then, you have a lambda. I chose that syntax because a shader stage can be imagined as a function of its
input, output and environment. I hesitated with returning the output, but it would make things harder for some stages
(i.e. geometry shaders), where they can write to the same output several times. In that lambda, every type is
automatically marked as unlifted (so input: TestInput and input: #TestInput are the same).
Once transformed, this single line is akin to something like this:
shades::stage::StageBuilder::new_vertex_shader(|mut __builder: shades::stage::StageBuilder::<#user_in_ty, #user_out_ty, #user_env_ty>, #input, #output| {
Everything else works the same way. Some fun example:
The generated code when the main function is found:
if name.to_string() == "main" {
let q = quote! {
// scope everything to prevent leaking them out
{
// function scope
let mut __scope: shades::scope::Scope<()> = shades::scope::Scope::new(0);
// roll down the statements
#body
// create the function definition
use shades::erased::Erased as _;
let erased = shades::fun::ErasedFun::new(Vec::new(), None, __scope.to_erased());
let fundef = shades::fun::FunDef::<(), ()>::new(erased);
__builder.main_fun(fundef)
}
};
q.to_tokens(tokens);
return;
}
If items:
impl ToTokens for IfItem {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let cond = &self.cond_expr;
let body = &self.body;
let q = quote! {
__scope.when(#cond, |__scope| {
#body
}) };
q.to_tokens(tokens);
match self.else_item {
Some(ref else_item) => {
let else_tail = &else_item.else_tail;
quote! { #else_tail }.to_tokens(tokens);
}
None => {
quote! { ; }.to_tokens(tokens);
}
}
}
Else tail items, which allows to add an if again (else if) or stop to else only. You will notice in ToTokens
implementation of IfItem that there is no ; added at the end of the .when() call unless there is no else
following, and that rule is kept enforced in ToTokens for ElseTailItem as well. See, static analysis at compile-time!:
impl ToTokens for ElseTailItem {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
match self {
ElseTailItem::If(if_item) => {
if_item.to_tokens_as_part_of_else(tokens);
}
ElseTailItem::Else { body, .. } => {
let q = quote! {
.or(|__scope| { #body });
};
q.to_tokens(tokens);
}
}
}
}
Once you get a Stage, you can use a writer to output a String, so that shades is compatible with any kind of
library / framework / engine that expects a string-based shader source.
For example, for GLSL:
let output = shades::writer::glsl::write_shader_to_str(&vertex_shader).unwrap();
luminanceEh, I wrote both crates, so obviously luminance has / will have a special support of shades. Remember the definition
of Environment above? It’s similar to Inputs and Outputs, too. luminance-derive already provides some derive
macros to automatically implement some traits, and it will provide a support to implement Inputs, Outputs and
Environment as well, without having to think about the indexes.
Of course, if something is not implemented correctly (a bug in shades-edsl, a bug in shades), debugging the output shaders will be harder, because everything is generated (names included) on the target side. It’s something to keep in mind.
You should also have a look at some similar yet different approaches:
rustup) and compile your code with. It’s
an interesting point but I wanted something more typed with a more seamless integration to the rest of my ecosystem
(i.e. luminance).Keep the vibes!
]]>
About the first point, I had a look at code that barely changed since I wrote it in 2016, and I’m not completely satisfied with it anymore. Lots of things happened ever since: I have a sharper vision of how things should be done, Rust itself has changed quite a lot (back in 2016, Rust 1.0 was just released!), we have more type system features that, back then, I was lacking, etc.
About the second point, some issues in the current version of luminance (v0.47 as writing this blog article) are
complex to solve, such as the Drop problem. Other parts of the API
are insanely complex, like the
TessBuilder related code.
I want all that to change, so I started working on a completely new graphics crate, just to see what kind of interface I could come up with. And I came up with something much, much easier. So I faced a dilemma: should I release that other crate and give up on luminance… or should I just port all the new knowledge and ideas from that crate into luminance?
Well, I have decided to keep luminance around and update it. It is going to take time, because luminance is an ecosystem of many crates, it has lots of examples (which is great to ensure that its features are still working correctly once I start redesign it) and because some features are obviously entangled. However, I think it’s worth it, because even though luminance is not as famous or widely used as wgpu, it has a special place in the Rust Graphics community.
So I’m going to blog about the redesign of luminance and it’s going to be a blog article series. I wanted to start with a very new exciting feature that I have been wanting for a very long time: compatible vertex types.
When you’re like me and are obsessed with tracking as much information as possible at compile-time, you start imagining
lots of features and ways to prevent people from doing things. Preventing and forbidding is actually much more complex
than allowing something. Think about it like this: assembly allows for everything and is not a complex language. It has
zero constructs, besides opcodes. However, as you add more constructs and abstractions, those abstractions restricts
the application domain; they forbid usage, in a constrained way. There’s a reason why, in Haskell, we use the term
Contraint — which is called, basically, trait bound in Rust.
The way luminance allows a programmer to render objects, which must have a vertex type V that must implement
Vertex. luminance-derive eases the process of implementing lots of traits from luminance. For instance:
#[repr(C)]
#[derive(Clone, Debug, Vertex)] // < notice the Vertex derive here
pub struct MyVertex {
position: Vector3<f32>,
normal: Vector3<f32>,
color: Vector3<u8>,
weight: Vector3<f32>,
}
This is great, because the user just has to focus on writing types and just using them. For vertex objects, which can be
imagined as big arrays / buffers of MyVertex here, there are types and functions allowing to basically render such
object, and those render functions expects MyVertex. However, rendering is a multi-stage process. As you may know, the
graphics pipeline contains different steps, and among the first ones, you have a vertex shader, which is responsible in
mapping vertices (to transform them, for instance). In luminance, vertex shaders are strongly-typed with the kind of
vertices they expect. That is to prevent a user from using a vertex shader that expects some vertex attributes that a
vertex object doesn’t have. Imagine that we write a vertex shader that expects a position and a normal but our
vertex object, which uses OtherVertex vertices, only has position. That would result in probably some visual
glitches or just a black screen.
So, we want to have something like this pseudocode:
type VertexShader<V>; // accept only vertex type V
type RenderVertexObjects<W>; // render only vertex type W
If we want to have a fully typed graphics pipeline, we must have V = W. However, that is going to be very annoying.
Imagine a vertex shader that only needs position, normal and color. Something like:
#[repr(C)]
#[derive(Clone, Debug, Vertex)]
pub struct ShaderVertexInput {
position: Vector3<f32>,
normal: Vector3<f32>,
color: Vector3<u8>,
}
This is a different type than MyVertex, so if we try to render our MyVertex vertex objects with a vertex shader that
works on ShaderVertexInput, it will get a type mismatch at compile-time. The only way to fix that is to create a copy
of the shader program that works with MyVertex and ignore the weight field. Meh. Waste of resources and duplication.
Everything we don’t want.
When you look at it, the vertex shader is going to use position, normal and color, but it doesn’t have to know
there are other vertex attributes. So it should be able, in theory, to work with MyVertex, since MyVertex has
position, normal and color…
One solution to fix the problem is to introduce a trait, CompatibleVertex, that we will use on the vertex shader code.
The vertex shader still has its ShaderVertexInput type, but it will not require vertex objects to use that type.
Instead, it will require vertex objects to use V and will require ShaderVertexInput: CompatibleVertex<V>. What it
means is that ShaderVertexInput must be included in V. Said otherwise, V must have all of ShaderVertexInput’s
fields (but it can have more).
pub trait CompatibleVertex<V> {}
Now the big question: how do we implement CompatibleVertex for a given concrete vertex type? Well, in the previous
version of luminance, that used to be implemented manually by users of luminance. It was both unsafe and not really
practical, so I just removed the concept (and luminance in version v0.47 and less is subject to the problem I
described above, where you can render a vertex object which vertex type is not compatible with what the shader expects).
Since rustc-1.51, const generics can be used at compile-time. The feature allows to manipulate integers at
compile-time. For instance:
pub struct Array<const N: usize> {
// …
}
Here, N is a const generics, and you use it with types like Array<3> or Array<14>. However, that’s currently
(rust-1.63) all you can do with a stable compiler… but we can do much more with a nightly compiler.
There is a feature called adt_const_params that allows using more types, and one type that is very important is
&'static str. Yes, you heard right. Constant strings. We can write something like this:
#![allow(incomplete_features)] // as of rustc-1.65 nightly, this is still required
#![feature(adt_const_params)]
fn hello<const N: &'static str>() {
println!("Hello {}!", N);
}
fn main() {
hello::<"world">();
}
That doesn’t seem like much, but this small change is going to save so many frustration in luminance, and fix our compatible vertex problems.
What we basically want for one vertex type to compatible with another (i.e. one is included into the other) is to ensure that the all the fields of one are present in the other. Let’s recall our vertex type definitions and trait:
#[repr(C)]
#[derive(Clone, Debug, Vertex)]
pub struct MyVertex {
position: Vector3<f32>,
normal: Vector3<f32>,
color: Vector3<u8>,
weight: Vector3<f32>,
}
#[repr(C)]
#[derive(Clone, Debug, Vertex)]
pub struct ShaderVertexInput {
position: Vector3<f32>,
normal: Vector3<f32>,
color: Vector3<u8>,
}
pub trait CompatibleVertex<V> {}
We can write a HasField trait like this:
pub trait HasField<const NAME: &'static str> {
type FieldType;
}
And now, we can implement that trait for all the fields of our vertex types:
// for MyVertex
impl HasField<"position"> for MyVertex {
type FieldType = Vector3<f32>;
}
impl HasField<"normal"> for MyVertex {
type FieldType = Vector3<f32>;
}
impl HasField<"color"> for MyVertex {
type FieldType = Vector3<u8>;
}
impl HasField<"weight"> for MyVertex {
type FieldType = f32;
}
// for ShaderInputVertex
impl HasField<"position"> for ShaderInputVertex {
type FieldType = Vector3<f32>;
}
impl HasField<"normal"> for ShaderInputVertex {
type FieldType = Vector3<f32>;
}
impl HasField<"color"> for ShaderInputVertex {
type FieldType = Vector3<u8>;
}
Now, we can write a more meaningful implementor for CompatibleVertex and ShaderInputVertex:
impl<V> CompatibleVertex<V> for ShaderInputVertex where
V: HasField<"position", FieldType = Vector3<f32>>
+ HasField<"normal", FieldType = Vector3<f32>>
+ HasField<"color", FieldType = Vector3<u8>>
{
}
Because MyVertex implements the three HasField trait bounds required, it can be used, as well as
ShaderInputVertex.
Obviously, because luminance-derive is a thing, people will never have to write any of those implementors. Actually,
deriving Vertex will do two things (besides what it already does) for a given vertex type:
HasField implementors for all of its field.CompatibleVertex implementor.What it means is that, as soon as you start using #[derive(Vertex)], you will automatically have compatible vertex
types behind the scenes working for you.
That feature is already, by itself, worth requiring a nightly compiler, as it brings the exact kind of type safety I
want for my code. And since I started with const generics strings, the next blog article will be about the redesign of
vertex objects (called Tess in the current version of luminance), since, you might have guessed, it will use const
generics strings to upload vertex data per-attribute, and map / update them.
Thanks for having read, and keep the vibe!
]]>The thing is… Times change. The more it passes, the more I become mature in what I do in the Haskell community. I’m a demoscener, and I need to be productive. Writing a whole 3D engine for such a purpose is a good thing, but I was going round and round in circles, changing the whole architecture every now and then. I couldn’t make my mind and help it. So I decided to stop working on that, and move on.
If you are a Haskell developer, you might already know Edward Kmett. Each talk with him is always interesting and I always end up with new ideas and new knowledge. Sometimes, we talk about graphics, and sometimes, he tells me that writing a 3D engine from scratch and release it to the community is not a very good move.
I’ve been thinking about that, and in the end, I agree with Edward. There’re two reasons making such a project hard and not interesting for a community:
Point 2 might be strange to you, but that’s often the case. Building a flexible 3D engine is a very hard and non-trivial task. Because of point 1, you utterly need to restrict things in order to get the required level of performance or design. There are people out there – especially in the demoscene world – who can build up 3D engines quickly. But keep in mind those engines are limited to demoscene applications, and enhancing them to support something else is not a trivial task. In the end, you might end up with a lot of bloated code you’ll eventually zap later on to build something different for another purpose – eh, demoscene is about going dirty, right?! ;)
So… Let’s go back to the basics. In order to include everyone, we need to
provide something that everyone can download, install, learn and use. Something
like OpenGL. For Haskell, I highly recommend
using gl. It’s built against the
gl.xml file – released by Khronos. If you need sound, you can use the
complementary library I wrote, using the same name convention,
al.
The problem with that is the fact that OpenGL is a low-level API. Especially for new comers or people who need to get things done quickly. The part that bothers – wait, no, annoys – me the most is the fact that OpenGL is a very old library which was designed two decades ago. And we suffer from that. A lot.
OpenGL is a stateful graphics library. That means it maintains a state, a context, in order to work properly. Maintaining a context or state is a legit need, don’t get it twisted. However, if the design of the API doesn’t fit such a way of dealing with the state, we come accross a lot of problems. Is there one programmer who hasn’t experienced black screens yet? I don’t think so.
The OpenGL’s API exposes a lot of functions that perform side-effects.
Because OpenGL is weakly typed – almost all objects you can create in
OpenGL share the same GL(u)int type, which is very wrong – you might end
up doing nasty things. Worse, it uses an internal binding system to select the
objects you want to operate on. For instance, if you want to upload data to a
texture object, you need to bind the texture before calling the texture upload
function. If you don’t, well, that’s bad for you. There’s no way to verify code
safety at compile-time.
You’re not convinced yet? OpenGL doesn’t tell you directly how to change things on the GPU side. For instance, do you think you have to bind your vertex buffer before performing a render, or is it sufficient to bind the vertex array object only? All those questions don’t have direct answers, and you’ll need to dig in several wikis and forums to get your answers – the answer to that question is “Just bind the VAO, pal.”
Several attempts to enhance that safety have come up. The first thing we
have to do is to wrap all OpenGL object types into proper types. For
instance, we need several types for Texture and Framebuffer.
Then, we need a way to ensure that we cannot call a function if the context is not setup for. There are a few ways to do that. For instance, indexed monads can be a good start. However, I tried that, and I can tell you it’s way too complicated. You end up with very long types that make things barely unreadable. See this and this for excerpts.
In my desperate quest of providing a safer OpenGL’s API, I decided to create a library from scratch called luminance. That library is not really an OpenGL safe wrapper, but it’s very close to being that.
luminance provides the same objects than OpenGL does, but via a safer way
to create, access and use them. It’s an effort for providing safe abstractions
without destroying performance down and suited for graphics applications. It’s
not a 3D engine. It’s a rendering framework. There’s no light, asset
managers or that kind of features. It’s just a tiny and simple powerful
API.
luminance is still a huge work in progress. However, I can already show an
example. The following example opens a window but doesn’t render anything.
Instead, it creates a buffer on the GPU and perform several simple operations
onto it.
-- Several imports.
import Control.Monad.IO.Class ( MonadIO(..) )
import Control.Monad.Trans.Resource -- from the resourcet package
import Data.Foldable ( traverse_ )
import Graphics.Luminance.Buffer
import Graphics.Luminance.RW
import Graphics.UI.GLFW -- from the GLFW-b package
import Prelude hiding ( init ) -- clash with GLFW-b’s init function
windowW,windowH :: Int
windowW = 800
windowH = 600
windowTitle :: String
windowTitle = "Test"
main :: IO ()
main = do
init
-- Initiate the OpenGL context with GLFW.
windowHint (WindowHint'Resizable False)
windowHint (WindowHint'ContextVersionMajor 3)
windowHint (WindowHint'ContextVersionMinor 3)
windowHint (WindowHint'OpenGLForwardCompat False)
windowHint (WindowHint'OpenGLProfile OpenGLProfile'Core)
window <- createWindow windowW windowH windowTitle Nothing Nothing
makeContextCurrent window
-- Run our application, which needs a (MonadIO m,MonadResource m) => m
-- we traverse_ so that we just terminate if we’ve failed to create the
-- window.
traverse_ (runResourceT . app) window
terminate
-- GPU regions. For this example, we’ll just create two regions. One of floats
-- and the other of ints. We’re using read/write (RW) regions so that we can
-- send values to the GPU and read them back.
data MyRegions = MyRegions {
floats :: Region RW Float
, ints :: Region RW Int
}
-- Our logic.
app :: (MonadIO m,MonadResource m) => Window -> m ()
app window = do
-- We create a new buffer on the GPU, getting back regions of typed data
-- inside of it. For that purpose, we provide a monadic type used to build
-- regions through the 'newRegion' function.
region <- createBuffer $
MyRegions
<$> newRegion 10
<*> newRegion 5
clear (floats region) pi -- clear the floats region with pi
clear (ints region) 10 -- clear the ints region with 10
readWhole (floats region) >>= liftIO . print -- print the floats as an array
readWhole (ints region) >>= liftIO . print -- print the ints as an array
floats region `writeAt` 7 $ 42 -- write 42 at index=7 in the floats region
floats region @? 7 >>= traverse_ (liftIO . print) -- safe getter (Maybe)
floats region @! 7 >>= liftIO . print -- unsafe getter
readWhole (floats region) >>= liftIO . print -- print the floats as an array
Those read/write regions could also have been made read-only or write-only. For such regions, some functions can’t be called, and trying to do so will make your compiler angry and throw errors at you.
Up to now, the buffers are created persistently and coherently. That might cause issues with OpenGL synchronization, but I’ll wait for benchmarks before changing that part. If benchmarking spots performance bottlenecks, I’ll introduce more buffers and regions to deal with special cases.
luminance doesn’t force you to use a specific windowing library. You can then
embed it into any kind of host libraries.
luminance is very young. At the moment of writing this article, it’s only 26 commits old. I just wanted to present it so that people know it exists will be released as soon as possible. The idea is to provide a library that, if you use it, won’t create black screens because of framebuffers incorrectness or buffers issues. It’ll ease debugging OpenGL applications and prevent from making nasty mistakes.
I’ll keep posting about luminance as I get new features implemented.
As always, keep the vibe, and happy hacking!
]]>This article is a sequel to Let’s talk about C++ constructors. Even though not required, reading that article first might give you more information on the motivation and my state of mind regarding C++ and software development.
This time, I want to talk about C++ exceptions. But talking about exceptions for the sake of talking about them has very little value. Let’s instead talk about error handling.
Imagine a function that takes some input, like a String, and outputs something else, like the length of the
string. Such a function is pure: its output depends only on its arguments. It cannot have any side-effect
nor return something else. It is also infallible as it can always return a valid length for all possible
values it can be called with. Imagine the following C++ implementation:
size_t get_len(std::string const & s) {
return s.length();
}
Even though C++ doesn’t have the concept of purity, let’s assume we can express the concept, just because it’s a powerful concept that we can apply to any language. The compiler will not know about it, but the way we design things does.
That function is pure and infallible. When you look at the signature, you see that it takes a read-only
reference on a std::string , and returns a size_t. And yes, I know about noexcept. However, this
specifier cannot be trusted. Consider:
#include <string>
void foo() {
throw std::runtime_error("That should be seen by the compiler, right?");
}
size_t get_len(std::string const & s) noexcept {
foo();
return s.length();
}
int main() {
std::string foo = "foo";
get_len(foo);
return 0;
}
Compile with (I’m running Archlinux):
g++ -std=c++2a -Wall -pedantic a.cpp
No compiler warning. Run it:
terminate called after throwing an instance of 'std::runtime_error'
what(): That should be seen by the compiler, right?
zsh: abort (core dumped) ./a.out
What that little snippet tells us is that even though get_len is annotated with noexcept… it can still
throw. Not directly in its body, but functions it calls may throw. When I was introduced to that keyword,
years ago, I was suspicious. Since C++ will throw exceptions for — erm — exceptional errors, such as
out of memory errors, then… even a noexcept function can still throw errors. Then, because of that,
noexcept cannot propagate downwards in your call stack. If A is noexcept and B may throw, then
calling B from A is valid in C++.
noexcept is just a documentation keyword and an opportunity for your compiler to optimize your code. Your
compiler can only emit warnings if you use the throw keyword directly in the body of the function that is
marked noexcept. Also, it’s important to notice that, given the team you work in, or the project you work
on, it’s possible to see the use of noexcept… like not at all. It’s all about convention; your compiler
will not enforce that… which is a problem to me. It’s a problem because more freedom means more opportunities
for people to make mistake. To forget about annotating a function with noexcept. Or, worse, it gives
opportunities to people who just don’t care and want to rush, making reviewing their code more challenging
than needed.
In my previous article, I’ve been criticized for not explaining enough what I mean about exceptional errors and that it was a highly subjective point. I’ll try to explain more in this section.
Imagine that you want to implement a function that will perform a lookup. If it finds the key you give as argument, it will return the associated object. However, what to do when the key is not there? If you read a bit my previous article, you know that I would use a sum type to encode the error in the type system. But let’s do it the C++ way. Let’s use exceptions.
Object lookup(Key const & key);
If you look at that signature, you’ll notice an important point. There is no error handling annotation. Most of the time, people will follow some guidelines to put that information in the documentation directly. However, several points:
noexcept code, as demonstrated in the
previous section — how do you know, when reading the code, that a call to this function can throw?try catch block. Do you assume the whole block as atomic? If so, do you lookup for the
documentation of all of the functions called in that block? What happens if you move that function out
of the block?Now, that just assumes a flat block. But it’s easy to guess that you will have to do that for the whole stack
of functions being called — i.e. as soon as you find a noexcept function in the stack… well nothing, you
have to go on, since a throw might be hidden in a function deeper in the stack.
Most of the time, the replies I get about that topic are, either:
N,
that there is a throw at level N - k. However, that seems like an impossible task to maintain. You might
forget to update the documentation if you stop throwing that exception or throw another object with a
different type, etc.try catch block.About the documentation and naming… it adds another problem: humans. We are fallible. You might work on a project that doesn’t document correctly. Or that doesn’t even have proper convention. Or several ones. When considering exceptions for error handling, I think it’s important to imagine what will happen in X months. After the codebase has become complex, large, with a lot of edge cases and possible errors. Maintainability should be a goal. No one enjoys having to read through the bodies of twenty functions to understand why their program crashed or why the GUI displays a pop-up with the content of an exception.
The sooner you can see an error, the better. If that sooner can be “compile-time”… why would you want to still push the error to the runtime? There are things I will never understand.
On the other side, consider sum types:
std::optional<Object> lookup(Key const & key);
Even though it’s still pretty bad to me because of how std::optional is made (have a look at my previous
article), it has the advantage of being typed. No documentation is needed — but please do document to
explain what can fail though — and your compiler can safely prevent you from doing bad things. Of course,
this is limited by how you use the std::optional, as C++ doesn’t have pattern-matching. But I would like
to reply to an argument I hear every now and then: it’s not because a better solution is not perfect that
it should be discarded to stay on your legacy solution. Imagine that you have a tool X with several
issues, {a, b, c, d}. Now imagine we suggest to switch to a new tool, Y, with issues {a, d} only.
Yes, you still have two issues… but you have less. In the case of exceptions vs. a type-system, in the
case of C++, yes, you can still call .value() on an empty std::optional and crash your program. But
you don’t have the problem of hidden and untracked error handling. You can simply use exceptions for
exceptional cases. Those cases that are not function boundaries nor edge cases. And yes, I do think that
most of the standard C++ exceptions, such as std::invalid_argument, are to be completely avoided.
But here, exceptions have an advantage if we stop there: they provide an error description.
Fixing that problem is pretty trivial with a strong type-system and algebraic data types. We want to use those to create a result type, that can either be something, or a typed error, that would contain exactly the same information you have in a regular exception.
C++ doesn’t really have that out of the box but it could be made, in the same way std::variant exists.
Imagine a hypothetical std::either type and let’s implement a function that parses something:
std::either<Something, ParseError> parse(std::string const & input);
That function returns either a successful object (Something) or an error (ParseError).
With that signature, it’s clear that the function can fail with ParseError. The point is that the caller
must do something about it. If they don’t know what to do with it — imagine a parser combinator or some
code that doesn’t know how to handle a parse error and requires its caller to handle the error — then
the function needs to abort and propagate the error upwards. That looks like a bit like the interface you
have with exceptions… but here, the interface is visible at the type-level.
Obviously, you cannot use throw to propagate upwards. You need to use return from your function. With
either a macro or some language construct, C++ could make that propagation smoother, but currently, it
doesn’t have a proper way to do it. So we’d be left with macros only, or manual propagation. Since C++
doesn’t have pattern-matching nor exhaustive patterns, it would be pretty hard to implement that mechanism
in a complete sound way. As with std::optional, it’s not perfect, but it would be slightly better than
using opaque exceptions.
One final point. Sometimes, I wonder what it would be like to just give up on my ideas of using a strong
type system in C++. The language is using exceptions and people are used to it. That’s the C++ way. So…
why not changing the way exceptions work so that they’re not opaque, and propagate upwards? I remember
the throw specifier, used to state what a function might throw. But again, it’s not enforced by compiler.
Worse, it’s being deprecated in C++ 2020.
I voluntarily omitted any reference to either Haskell, Rust or any language like that so that people don’t think I’m trying to compare C++ to another language. I’m having a look on C++ after almost two decades using it and what else I’ve learned. The situation is, to me, frustrating. Because yes, whatever the good arguments against exceptions, people still use them and error handling in C++ is still about exceptions. So you still can have constructors that fail. You still depend a lot on documentation and your compiler cannot tell you when something is not okay. We are all fallible, way more than we think.
]]>git stash. If you don’t know that command yet,
git stash is used to move all the changes living in your staging area into a special place: the
stash.
The stash is a temporary area working like a stack. You can push changes onto it via git stash
or git stash save; you can pop changes from top with git stash pop. You can also apply a very
specific part of the stack with git stash apply <stash id>. Finally you can get the list of all
the stashes with git stash list.
We often use the git stash command to stash changes in order to make the working directory clear
again so that we can apply a patch, pull some changes, change branch, and so on. For those purposes,
the stash is pretty great.
However, I often forget about my stashes – I know I’m not the only one. Sometimes, I stash something and go to cook something or just go out, and when I’m back again, I might have forgotten about what I had stashed, especially if it was a very small change.
My current prompt for my shell, zsh, is in two parts. I set the PS1
environnment variable to set the regular prompt, and the RPROMPT environnment variable to set a
reversed prompt, starting from the right of the terminal. My reversed prompt just performs a git
command to check whether we’re actually in a git project, and get the current branch. Simple, but
nice.
I came up to the realization that I could use the exact same idea to know whether I have stashed changes so that I never forget them! Here’s a screenshot to explain that:

As you can see, my prompt now shows me how many stashed changes there are around!
I share the code I wrote with you. Feel free to use it, modify it and share it as well!
# …
function gitPrompt() {
# git current branch
currentBranch=`git rev-parse --abbrev-ref HEAD 2> /dev/null`
if (($? == 0))
then
echo -n "%F{green}$currentBranch%f"
fi
# git stash
stashNb=`git stash list 2> /dev/null | wc -l`
if [ "$stashNb" != "0" ]
then
echo -n " %F{blue}($stashNb)%f"
fi
echo ''
}
PS1="%F{red}%n%F{cyan}@%F{magenta}%M %F{cyan}%~ %F{yellow}%% %f"
RPROMPT='$(gitPrompt)'
# …
Have fun!
]]>luminance started as a Haskell package, extracted from a “3D engine” I had been working on for a while called quaazar. I came to the realization that I wasn’t using the Haskell garbage collector at all and that I could benefit from using a language without GC. Rust is a very famous language and well appreciated in the Haskell community, so I decided to jump in and learn Rust. I migrated luminance in a month or two. The mapping is described in this blog entry.
I’ve been writing 3D applications for a while and I always was frustrated by how OpenGL is badly designed. Let’s sum up the lack of design of OpenGL:
GLint, GLuint or GLbitfield are
all defined as aliases to primary and integral types (i.e. something like
typedef float GLfloat). Try it with grep -Rn "typedef [a-zA-Z]* GLfloat" /usr/include/GL. This
leads to the fact that framebuffers, textures, shader stages, shader program or even
uniforms, etc. have the same type (GLuint, i.e. unsigned int). Thus, a function like
glCompileShader expects a GLuint as argument, though you can pass a framebuffer, because it’s
also represented as a GLuint – very bad for us. It’s better to consider that those are just
untyped – :( – handles.glCompileShader and a framebuffer. That
means OpenGL implementations have to check against all the values you’re passing as arguments to
be sure they match the type. That’s basically several tests for each call of an OpenGL
function. If the type doesn’t match, you’re screwed and see the next point.glGetError function,
adding a side-effect, preventing parallelism, etc.The goal of luminance is to fix most of those issues by providing a safe, stateless and elegant graphics framework. It should be as low-level as possible, but shouldn’t sacrifice runtime performances – CPU charge as well as memory bandwidth. That is why if you know how to program with OpenGL, you won’t feel lost when getting your feet wet with luminance.
Because of the many OpenGL versions and other technologies (among them, vulkan), luminance has an extra aim: abstract over the trending graphics API.
In luminance, all graphics resources – and even concepts – have their own respective type. For
instance, instead of GLuint for both shader programs and textures, luminance has Program and
Texture. That ensures you don’t pass values with the wrong types.
Because of static warranties provided by compile-time, with such a scheme of strong-typing, the runtime shouldn’t have to check for type safety. Unfortunately, because luminance wraps over OpenGL in the luminance-gl backend, we can only add static warranties; we cannot remove the runtime overhead.
luminance follows the Rust conventions and uses the famous Option and Result types to specify
errors. You will never have to check against a global error flag, because this is just all wrong.
Keep in mind, you have the try! macro in your Rust prelude; use it as often as possible!
Even though Rust needs to provide an exception handler – i.e. panics – there’s no such thing as exceptions in Rust. The
try!macro is just syntactic sugar to:
match result { Ok(x) => x, Err(e) => return e }
luminance is stateless. That means you don’t have to bind an object to be able to use it. luminance takes care of that for you in a very simple way. To achieve this and keep performances running, it’s required to add a bit of high-level to the OpenGL API by leveraging how binds should happen.
Whatever the task you’re trying to reach, whatever computation or problem, it’s always better to gather / group the computation by batches. A good example of that is how magnetic hard drive disks work or your RAM. If you spread your data across the disk region (fragmented data) or across several non-contiguous addresses in your RAM, it will end up by unnecessary moves. The hard drive’s head will have to go all over the disk to gather the information, and it’s very likely you’ll destroy the RAM performance (and your CPU caches) if you don’t put the data in a contiguous area.
If you don’t group your OpenGL resources – for instances, you render 400 objects with shader A, 10 objects with shader B, then 20 objects with shader A, 32 objects with shader C, 349 objects with shader A and finally 439 objects with shader B, you’ll add more OpenGL calls to the equation – hence more global state mutations, and those are costly.
Instead of this:
luminance forces you to group your resources like this:
This is done via types called Pipeline, ShadingCommand and RenderCommand.
A Pipeline gathers shading commands under a Framebuffer. That means that all ShadingCommand
embedded in the Pipeline will output to the embedded Framebuffer. Simple, yet powerful, because
we can bind the framebuffer when executing the pipeline and don’t have to worry about framebuffer
until the next execution of another Pipeline.
A ShadingCommand gathers render commands under a shader Program along with an update function.
The update function is used to customize the Program by providing uniforms – i.e. Uniform.
If you want to change a Programs Uniform once a frame – and only if the Program is only called
once in the frame – it’s the right place to do it.
All RenderCommand embedded in the ShadingCommand will be rendered using the embedded shader
Program. Like with the Pipeline, we don’t have to worry about binding: we just have to use the
embedded shader program when executing the ShadingCommand, and we’ll bind another program the next
time a ShadingCommand is ran!
A RenderCommand gathers all the information required to render a Tessellation, that is:
Program being in use – so that each object can have different
properties used in the shader programTessellation to renderTessellation to renderTessellation contains any)Shaders are written in… the backend’s expected format. For OpenGL, you’ll have to write GLSL.
The backends automatically inserts the version pragma (#version 330 core for OpenGL 3.3 for
instance). In the first place, I wanted to migrate cheddar, my Haskell shader EDSL. But… the sad
part of the story is that Rust is – yet – unable to handle that kind of stuff correctly. I started
to implement an EDSL for luminance with macros. Even though it was usable, the error handling is
seriously terrible – macros shouldn’t be used for such an important purpose. Then some rustaceans
pointed out I could implement a (rustc) compiler plugin. That enables the use of new constructs
directly into Rust by extending its syntax. This is great.
However, with the hindsight, I will not do that. For a very simple reason. luminance is, currently, simple, stateless and most of all: it works! I released a PC demo in Köln, Germany using luminance and a demoscene graphics framework I’m working on:
While developping Céleri Rémoulade, I decided to bake the shaders directly into Rust – to get used
to what I had wanted to build, i.e., a shader EDSL. So there’re a bunch of constant &'static str
everywhere. Each time I wanted to make a fix to a shader, I had to leave the application, make the
change, recompile, rerun… I’m not sure it’s a good thing. Interactive programming is a very good
thing we can enjoy – yes, even in strongly typed languages ;).
I saw that gfx doesn’t have its own shader EDSL either and requires you to provide several shader implementations (one per backend). I don’t know; I think it’s not that bad if you only target a single backend (i.e. OpenGL 3.3 or Vulkan). Transpiling shaders is a thing, I’ve been told…
sneaking out…
Feel free to dig in the code of Céleri Rémoulade here. It’s demoscene code, so it had been rushed on before the release – read: it’s not as clean as I wanted it to be.
I’ll provide you with more information in the next weeks, but I prefer spending my spare time writing code than explaining what I’m gonna do – and missing the time to actually do it. ;)
Keep the vibe!
]]>Nowadays, there is a cool concept out there in the Functional Programming world which is called FRP. It stands for Functional Reactive Programming and is a pretty decent way to make event-driven programs.
The problem with FRP is that, beside Wikipedia, Haskell.org and a few other resources, like Conal Elliott’s papers, we lack learning materials. Getting into FRP is really not trivial and because of the lack of help, you’ll need to be brave to get your feet wet.
Because I found it hard learning it from scratch and because I think it’s a good thing to pass knowledge by, I decided to write a few about it so that people can learn via an easier path.
I’ll be talking about netwire, which is not the de-facto library to use in Haskell, because, eh… we don’t have any. However, netwire exposes a lot of very interesting concepts and helped me to understand more general abstractions. I hope it’ll help you as well. :)
In traditional event-driven programming codebase, you’d find constructions such as events polling (when you explicitely retrieve last occurring events), callbacks and event handlers. GLFW is a very famous example of callback uses for event-driven programming. Such functions as glfwSetWindowCloseCallback require you to pass a callback that will be called when the event occurs. While that way to go seems nice, it’s actually error-prone and ill-formed design:

However, it’s not black or white. Callbacks are mandatory. They’re useful, and we’ll see that later on.
FRP is a new way to look at event-driven programming. Instead of representing
reaction through callbacks, we consume events over time. Instead of building a
callback that will be passed as reaction to the setPushButtonCallback
function, we consume and transform events over time. The main idea of FRP could
be summed up with the following concepts:
According to Wikipedia, a behavior is the range of actions and mannerisms made by individuals, organisms, systems, or artificial entities in conjunction with themselves or their environment, which includes the other systems or organisms around as well as the (inanimate) physical environment. If we try to apply that to a simple version of FRP that only takes into account the time as external stimulus, a behavior isany kind of value that consumes time. What’s that? Well…
newtype Behavior a = Behavior { stepBehavior :: Double -> a }
A behavior is a simple function from time (Double) to a given value. Let’s take an
example. Imagine you want to represent a cube rotating around the X axis. You
can represent the actual rotation with a Behavior Rotation, because the angle of
rotation is linked to the time:

rotationAngle :: Behavior Rotation
rotationAngle = Behavior $ \t -> rotate xAxis t
Pretty simple, see?! However, it’d would more convenient if we could chose the type
of time. We don’t really know what the time will be in the final application. It
could be the current UTC time, it could be an integral time (think of a stepped
discrete simulation), it could be the monotonic time, the system time, something
that we don’t even know. So let’s make our Behavior type more robust:
newtype Behavior t a = Behavior { stepBehavior :: t -> a }
Simple change, but nice improvement.
That is the typical way to picture a behavior. However, we’ll see later that the implementation is way different that such a naive one. Keep on reading.
An event is something happening at some time. Applied to FRP, an event is a pair of time – giving the time of occurrence – and a carried value:
newtype Event t a = Event { getEvent :: (t,a) }
For instance, we could create an event that yields a rotation of 90° around X at 35 seconds:
rotate90XAt35s :: Event Float Rotation
rotate90XAt35s = Event (35,rotate xAxis $ 90 * pi / 180)
Once again, that’s the naive way to look at events. Keep in mind that events have time occurrences and carry values.
You switch your behavior every time. Currently, you’re reading this paper, but you may go grab some food, go to bed, go to school or whatever you like afterwards. You’re already used to behavior switching because that’s what we do every day in a lifetime.
However, applying that to FRP is another thing. The idea is to express this:
“Given a first behavior, I’ll switch to another behavior when a given event occurs.”
This is how we express that in FRP:
switch :: Behavior t a -> Event t (Behavior t a) -> Behavior t a
Let me decrypt switch for you.
The first parameter, a Behavior t a, is the
initial behavior. For instance, currently, you’re reading. That could be the
first behavior you’d pass to switch.
The second parameter, an Event t (Behavior t a), is an event that yields a
new Behavior t a. Do you start to get it? No? Well then:
switch reading finished
reading is the initial behavior, and finished is an event that occurs when
you’re done reading. switch reading finished is then a behavior that equals
to reading until finished happens. When it does, switch reading finished
extracts the behavior from the event, and uses it instead.
I tend to think switch is a bad name, and I like naming it until:
reading `until` finished
Nicer isn’t it?! :)
Stepping is the act of passing the input – i.e. the time t in our case – down
to the Behavior t a and extract the resulting a value. Behaviors are
commonly connected to each other and form a reactive network.
That operation is also often refered to as reactimation in certain
implementations, but is more complex than just stepping the world. You don’t
have to focus on that yet, just keep in mind the reactimate function. You
might come across it at some time.
Everything you read in that paper until now was just pop-science so that you understand the main idea of what FRP is. The next part will cover a more decent and real-world implementation and use of FRP, especially efficient implementations.
The first common error a lot of programmers make is trying to write algorithms or libraries to solve a problem they don’t even know. Let’s then start with an example so that we can figure out the problem.
Let’s say we want to be able to control a camera with a keyboard:
W would go forwardS would go backwardA would left strafeD would right strafeR would go upF would go down
Let’s write the Input type:
data Input
= W
| S
| A
| D
| R
| F
| Quit
deriving (Eq,Read,Show)
Straight-forward. We also have a function that polls events from IO:
pollEvents :: IO [Input]
pollEvents = fmap treatLine getLine
where
treatLine = concatMap (map fst . reads) . words
We use [Input] because we could have several events at the same time
(imagine two pressed keys). The function is using dumb implementation
in order to abstract over event polling. In your program, you won’t use
getLine but a function from SDL
or similar.
And the camera:
newtype Camera = Camera { _cameraPosition :: V3 Float } deriving (Eq,Read,Show)
makeLenses ''Camera
V3 is a type from linear.
You’ll need that lib then, and import Linear.V3 to make the Camera compile.
You’ll also need lens and the GHC
TemplateHaskell extension enabled as well as import Control.Lens.
Ok, let’s react to events!
The idea is to use some kind of state we’ll change on an event. In our case the state is pretty simple:
data AppSt = AppSt {
_appCamera :: Camera
} deriving (Eq,Read,Show)
makeLenses ''AppSt
updateAppSt :: AppSt -> Input -> Maybe AppSt
updateAppSt appst input = case input of
W -> Just $ appst & appCamera . cameraPosition . _z -~ 0.1
S -> Just $ appst & appCamera . cameraPosition . _z +~ 0.1
A -> Just $ appst & appCamera . cameraPosition . _x -~ 0.1
D -> Just $ appst & appCamera . cameraPosition . _x +~ 0.1
F -> Just $ appst & appCamera . cameraPosition . _y -~ 0.1
R -> Just $ appst & appCamera . cameraPosition . _y +~ 0.1
Quit -> Nothing
A lot of boilerplate on updateAppSt, but that doesn’t matter that much.
The idea is to modify the application state and just return it for all
inputs but Quit, in which case we return Nothing to express the wish
to quit the application.
I’ve been using that idea for a while. It’s simple, nice and neat, because
we don’t spread IO actions in our program logic, which remains then pure.
That’s a very good way of doing it, and in most cases, it’ll even be
sufficient. However, that idea suffers from a serious issue: it doesn’t
scale.
Who has only one camera? No one. You have a camera – maybe more than just
one, lights, objects, terrains, AI, sounds, assets, maps and so on and so
forth. Our little AppSt type would explode as we add objects. That
doesn’t scale at all. You could, though, go on and add all your objects
in your AppSt – I did that once, it was a pretty harsh experience.
Furthermore, imagine you want to add a new behavior to the camera, like
being able to handle the mouse cursor move – Input being augmented, of
course. You’d need to change / add lines in our updateAppSt function.
Imagine how messy updateAppSt would be… That would, basically, gather
all reactions into a single function. Not neat.
FRP enables us to use our reactive values as if they are regular values. You can add reactive values, you can substract them, combine them in any way you want. The semantics of your values should be true for the reactive values.
Typically, with FRP, you don’t have event handlers anymore. The codebase can then grow sanely without having to accumulate big states every now and then. FRP applications scale and compose pretty well.
Let’s start with a simple FRP implementation for our example.
Let’s start with the behavior:
newtype Behavior t a = Behavior { stepBehavior :: t -> a }
How could we implement our camera’s behavior with that? We actually can’t since we don’t have any ways to pass events.
“I guess we could build a
Behavior t Cameraby passing ourInputto the initial function?”
Something like this?
camera inputs = Behavior $ \_ -> -- implement the behavior with inputs
That could be a way to go, yeah. However, how would you implement
switching with that? Remember the type of until:
until :: Behavior t a -> Event (Behavior t a) -> Behavior t a
camera is not a behavior, it’s a function from events to a
behavior. You have to apply the events on camera in order to get its
behavior. Once you’ve done that, you cannot pass events to the next
behavior. What a pity. That is more a configured behavior than a
behavior consuming inputs / events.
With the current Behavior t a implementation, a behavior network is
reduced to a function t -> a, basically. Then, the only stimulus you
got from outside is… time. We lack something.
“A way to forward events?”

Yes! But more mainly, a way to extend our Behavior t a with inputs!
Don’t get me wrong, we are not talking about a reactive value here.
We are talking about a reactive relationship:
newtype Behavior t a b = Behavior { stepBehavior :: t -> a -> b }
That’s way better! Our new behavior represents a relationship between
two reactive objects. The b is our old a, and the new a is the
input! Let’s see what we can do with that.
camera :: Behavior t [Input] Camera
camera = Behavior (const treatEvents)
where
treatEvents events
| W `elem` events = Camera $ V3 0 0 (-0.1)
| S `elem` events = Camera $ V3 0 0 0.1
| otherwise = Camera $ V3 0 0 0
That is not exactly what we intented to express. Here, if we push the
W button, we just put the camera in V3 0 0 (-0.1), while we’d like
to move it forward. That is due to the fact we need switching.
The idea is the following: the initial behavior is idleing. We just
don’t do anything. Then, we switch to a given behavior when a given
event occurs. We’ll need recursion so that we can ping-pong between
behaviors. Let’s write the idleing behavior:
idleing :: Behavior t ([Input],Camera) Camera
idleing = Behavior (const snd)
That behavior requires as input our Input events list and a
Camera and simply returns the Camera. Pretty nice.
How do we switch then? We need Event. Consider this:
newtype Event t a = Event { getEvent :: (t,a) }
In order to switch, we need a to be a behavior. In the first place,
we’ll create several Event t [Input] and pass them as input to the
behavior network. How could we change the [Input] to something more
interesting? Simple: Functor!
instance Functor (Event t) where
fmap f (Event e) = Event (fmap f e)
Note: because of
Event t abeing anewtype, you should rather use the GHCGeneralizedNewtypeDerivingextension to automatically let GHC infer the instance for you.
newtype Event t a = Event { getEvent :: (t,a) } deriving (Functor)
Then, we can use the Functor instance to change the type of the
carried value of the event. That’s great because we don’t change the
occurrence time, only the carried value. Transforming events is really
important in FRP. We can then transform the [Input] into a single
behavior:
inputToBehavior i = case i of
W -> Behavior $ \_ (_,cam) -> cam & cameraPosition . _z -~ 0.1
S -> Behavior $ \_ (_,cam) -> cam & cameraPosition . _z +~ 0.1
A -> -- and so on
D -> -- and so forth
F -> -- ;)
R -> -- ...
_ -> Behavior $ \_ (_,cam) -> cam
Pretty simple, see? When we push W, we go forward forever. We could
have implemented the function above with another until call in order
to go back to idleing, making some kind of behavior loop.
However, switching is fairly poorly implemented here. It’s not very efficient, and requires a ton of boilerplate.
There is a very cool type out there called Auto, used to implement
automatons.
newtype Auto a b = Auto { runAuto :: a -> (b,Auto a b) }
An Auto a b is a function-like structure. It has an input and an
output. The difference with a regular function is the fact it also has
a secondary output, which is another Auto a b. That is, it’s the next
automaton to be used.
Auto a b wraps pretty cool concepts, such as locally defined states.
It’s also a great ally when implementing switching in a FRP system,
because we can easily state that Behavior ≃ Auto. A Behavior is
a function from the environment state to the next reactive value, and
has also another output representing what to do “next”.
Let’s then change our Behavior type to make it look like a bit more
like Auto:
newtype Behavior t a b = Behavior { stepBehavior :: t -> a -> (b,Behavior t a b) }
Yeah, that’s it! That’s a pretty good start!
Before going on, I’d like to introduce those scary abstractions you are afraid of. Because they’re actually not. They’re all simple. At least for Haskell purposes.
Note: I do know we could simply use the GeneralizedNewtypeDeriving` extension but I want to detail all the implementation, so we’ll see how to implement all the nice abstractions.
Arrows are a generalization of functions along the axis of computation. A computation has inputs and outputs. So does a behavior.
Although arrows are not really used in Haskell, they’re ultra simple (themselves and the common combinators built over the abstraction) and useful in some cases.
In order to implement arrows, we need to provide code for both the arr
function, which type is arr :: (Arrow a) => (b -> c) -> a b c and
first, which type is first :: (Arrow a) => a b c -> a (b,d) (c,d).
arr is used to lift a common function into the arrowized version, and
first takes an arrow which takes a value as input and exposes an arrow
that takes a pair as input, applying the given function on the first
value of the pair. Let’s implement that:
instance Arrow (Behavior t) where
arr f = fix $ \r -> Behavior $ \t a -> (f a,r)
first f = Behavior $ \t (b,d) ->
let (c,fn) = stepBehavior f t b
in ((c,d),first fn)
A category
basically exposes two concepts: composition and identity. In our case,
the identity represents a constant behavior in time and the composition
composes two behaviors in time. Let’s implement Category by providing
implementation for both id and (.):
instance Category (Behavior t) where
id = arr id
x . y = Behavior $ \t a ->
let (yr,yn) = stepBehavior y t a
(xr,xn) = stepBehavior x t yr
in (xr,xn . yn)
Note: because of Prelude exporting specialized implementation
of id and (.) – the function ones – you should hide them in order
to implement Category:
import Prelude hiding ( (.), id )
A semigroup is a pretty cool algebraic structure used in Haskell to represent “anything that associates”. It exposes an associative binary function over a set. In the case of behaviors, if two behaviors output semigroup values, we can associates the behaviors to build a single one.
A Semigroup is implemented via a single typeclass method, (<>).
Let’s do that for behaviors:
instance (Semigroup b) => Semigroup (Behavior t a b) where
x <> y = Behavior $ \t a ->
let (xr,xn) = stepBehavior x t a
(yr,yn) = stepBehavior y t a
in (xr <> yr,xn <> yn)
Simple and neat.
You might already know that one since I talked about it a few lines ago,
but let’s write the instance for our Behavior:
instance Functor (Behavior t a) where
fmap f b = Behavior $ \t a ->
let (br,bn) = stepBehavior b t a
in (f br,fmap f bn)
Pretty cool eh?
A very known one too. Let’s see how we could implement Applicative:
instance Applicative (Behavior t a) where
pure = arr . const
f <*> x = Behavior $ \t a ->
let (fr,fn) = stepBehavior f t a
(xr,xn) = stepBehavior x t a
in (fr xr,fn <*> xn)
This one is special. You don’t have to know what a profunctor is, but eh, you should, because profunctors are pretty simple to use in Haskell, and are very useful. I won’t explain what they are – you should have a look at this article for further details.
If you do know them, here’s the implementation for dimap:
instance Profunctor (Behavior t) where
dimap l r x = Behavior $ \t a ->
let (xr,xn) = stepBehavior x t (l a)
in (r xr,dimap l r xn)
Behaviors consume environment state and have outputs. However, they sometimes just don’t. They don’t output anything. That could be the case for a behavior that only emits during a certain period of time. It could also be the case for a signal function that’s defined on a given interval: what should we output for values that lie outside?
Such a scenario is called inhibition. There’re several solutions
to implement inhibition. The simplest and most famous one is by
using Maybe as a wrapper over the output. Like the following:
Behavior t a (Maybe b)
If (Maybe b) is Nothing, the output is undefined, then the
behavior inhibits.
However, using a bare Maybe exposes the user directly
to inhibition. There’s another way to do that:
newtype Behavior t a b = Behavior { stepBehavior :: t -> a -> Maybe (b,Behavior t a b) }
Here we are. We have behaviors that can inhibit. If a behavior doesn’t
inhibit, it returns Just (output,nextBehavior), otherwise it
outputs Nothing and inhibits forever.
Exercise: try to reimplement all the above abstractions with the new type of
Behavior.
We can add a bunch of other interesting functions:
dead :: Behavior t a b
dead = Behavior $ \_ _ -> Nothing
one :: b -> Behavior t a b
one x = Behavior $ \_ _ -> Just (x,dead)
dead is a behavior that inhibits forever. That is, it doesn’t
produce any value at any time.
one x produces x once, and then inhibits forever. That’s a nice
combinator to use when you want to pulse values in time. We’ll
see later that it’s very useful to represent discrete events, like
key presses or mouse motion.
However, inhibiting can be useful. For instance, we can implement a new kind of behavior switching using inhibition. Let’s try to implement a function that takes two behaviors and switches to the latter when the former starts inhibiting:
revive :: Behavior t a b -> Behavior t a b -> Behavior t a b
revive x y = Behavior $ \t a -> case stepBehavior x t a of
Just (xr,xn) -> return (xr,revive xn y)
Nothing -> stepBehavior y t a
(~>) :: Behavior t a b -> Behavior t a b -> Behavior t a b
(~>) = revive
(~>) is a handy alias to revive. Then, a ~> b is a behavior
that is a until it inhibits, afterwhile it’s b. Simple, and
useful.
In netwire, revive – or (~>) – is (-->). There’s
also an operator that does the opposite thing: (>--).
a >-- b is a until b starts producing – i.e. until b
doesn’t inhibit anymore.
Exercise: write the implementatof of
(>~), our version for netwire’s(>--).

Now you have a better idea of how you could implement a behavior, let’s talk about netwire’s one.
netwire’s behavior type is called Wire. It’s actually:
Wire s e m a b
s is the session time – it’s basically a type that’s used
to extract time. e is the inhibition value. m is a inner
monad – yes, you can use monadic code within netwire, which is
not really useful actually, except for Reader, I guess. And
a and b are respectively inputs and outputs.
“What is that inhibition type?”
Yeah, netwire doesn’t use Maybe for inhibition. Picture
Wire as:
newtype Wire s e m a b = Wire { stepWire :: s -> a -> m (Either e (b,Wire s e m a b)) }
Instead of using Maybe (b,Wire s e m a b), it uses Either.
Some functions require e to be a Monoid. I guess netwire uses
that to accumulate during inhibition. I don’t see decent use
cases of such a feature, but it’s there. I tend to use this kind
of wire in all my uses of netwire:
Wire s () Identity a b -- I think this is the most common type of wire
Keep in mind that although you can set m to IO, it’s not
what netwire – and FRP – was designed for.
What about events? Well, netwire exposes events as a home-made
Maybe:
data Event a
= Event a
| NoEvent
deriving (Eq,Show)
instance Functor Event where
fmap f e = case e of
Event a -> Event (f a)
NoEvent -> NoEvent
That’s actually enough, because we can attach Event a to time
occurences with Behavior t b a. You’ll find every now and then
functions using Wire s e m (Event a) b, for instance. You should
get used to that as you write toy examples, and real-world ones,
of course.
What a trek… As you can see, we were able to approach netwire’s implementation understanding pretty closely. There are a few concepts I haven’t covered – like intrinsic switches, continuable switches, deferred switches… – but I don’t pretend having a comprehensive FRP article. You’ll have to dig in a bit more ;)
I’ll write another article about FRP and netwire to implement the camera example with netwire so that you can have a concrete example.
]]>OpenGL allows programmers to send vertices to the GPU through what is called a vertex array. Vertex specification is performed through several functions, operating on several objects. You need, for instance, a vertex buffer object, an index buffer object and a vertex array object. The vertex buffer stores the vertices data.

For instance, you could imagine a teapot as a set of vertices. Those vertices have several attributes. We could use, for instance, a position, a normal and a bone index. The vertex buffer would be responsible of storing those positions, normals and bone indices. There’re two ways to store them:
I’ll explain those later on. The index buffer stores integral numbers –
mainly set to unsigned int – that index the vertices, so that we can connect
them and create lines, triangles or more complex shapes.
Finally, the vertex array object is a state object that stores links to the two buffers and makes a connection between pointers in the buffer and attribute indices. Once everything is set up, we might only use the vertex array object. The exception is when we need to change the geometry of an object. We need to access the vertex buffer and the index buffer and upload new data. However, for now, that feature is disabled so that the buffers are not exposed to the programmer. If people think that feature should be implemented, I’ll create specialized code for that very purpose.
Interleaved arrays might be the most simple to picture, because you use such arrays every day when programming. Let’s imagine you have the following type in Haskell:
data Vertex = Vertex {
vertPos :: X
, vertNor :: Y
, vertBoneID :: Z
} deriving (Eq,Show)
Now, the teapot would have several vertices. Approximately, let’s state the teapot has five vertices – yeah, ugly teapot. We can represent such vertices in an interleaved array by simply recording them in a list or an array:

As you can see, the attributes are interleaved in memory, and the whole pattern is cycling. That’s the common way to represent an array of struct in a lot of languages, and it’s very natural for a machine to do things like that.
The deinterleaved version is:

As you can see, with deinterleaved arrays, all attributes are extracted and
grouped. If you want to access the third vertex, you need to read the third X,
the third Y and the third Z.
Both the methods have advantages and drawbacks. The cool thing about deinterleaved arrays is that we can copy huge regions of typed memory at once whilst we cannot with interleaved arrays. However, interleaved arrays store continuous structures, so writing and reading a structure back might be faster.
An important point to keep in mind: because we plan to pass those arrays to OpenGL, there’s no alignment restriction on the structure. That is, everything is packed, and we’ll have to pass extra information to OpenGL to tell it how to advance in memory to correctly build vertices back.
I think I haven’t told you yet. I have a cool type in
luminance: the (:.) type. No, you
don’t have to know how to pronounce that. I like to call it the gtuple
type, because it’s a generalized tuple. You can encode (a,b), (a,b,c) and
all kind of tuples with (:.). You can even encode single-typed infinite
tuple! – a very special kind of list, indeed.
data a :. b = a :. b
infixr 6 :.
-- a :. b is isomorphic to (a,b)
-- a :. b :. c is isomorphic to (a,b,c)
newtype Fix f = Fix (f (Fix f)) -- from Control.Monad.Fix
type Inf a = Fix ((:.) a) -- infinite tuple!
Pretty simple, but way more powerful than the regular, monomorphic tuples. As
you can see, (:.) is a right-associative. That means that
a :. b :. c = a :. (b :. c).
That type will be heavily used in
luminance, thus you should get your fet
wet with it. There’s actually nothing much to know about it. It’s a Functor.
I might add other features to it later on.
The cool thing about (:.) is that we can provide a Storable instance for
packed memory, as OpenGL requires it. Currently, the Storable instance
is implemented like this:
instance (Storable a,Storable b) => Storable (a :. b) where
sizeOf (a :. b) = sizeOf a + sizeOf b
alignment _ = 1 -- packed data
peek p = do
a <- peek $ castPtr p
b <- peek . castPtr $ p `plusPtr` sizeOf (undefined :: a)
pure $ a :. b
poke p (a :. b) = do
poke (castPtr p) a
poke (castPtr $ p `plusPtr` sizeOf (undefined :: a)) b
As you can see, the alignment is set to 1 to express the fact the memory
is packed. The peek and poke functions use the size of the head of the tuple
to advance the pointer so that we effectively write the whole tuple in packed
memory.
Then, let’s rewrite our Vertex type in terms of (:.) to see how it’s going
on:
type Vertex = X :. Y :. Z
If X, Y and Z are in Storable, we can directly poke one of our
Vertex into a luminance buffer! That is, directly into the GPU buffer!
Keep in mind that the Storable instance implements packed-memory uploads and
reads, and won’t work with special kinds of buffers, like shader storage ones,
which require specific memory alignment. To cover them, I’ll create specific
typeclasses instances. No worries.
Creating a vertex array is done through the function createVertexArray. I
might change the name of that object – it’s ugly, right? Maybe Shape, or
something cooler!
createVertexArray :: (Foldable f,MonadIO m,MonadResource m,Storable v,Traversable t,Vertex v)
=> t v
-> f Word32
-> m VertexArray
As you can see, the type signature is highly polymorphic. t and f represent
foldable structures storing the vertices and the indices. And that’s all.
Nothing else to feed the function with! As you can see, there’s a typeclass
constraint on v, the inner vertex type, Vertex. That constraint ensures the
vertex type is representable on the OpenGL side and has a known vertex
format.
Disclaimer: the Traversable constraint might be relaxed to be Foldable
very soon.
Once tested, I’ll move all that code from the unstable branch to the master
branch so that you guys can test it. :)
I eventually came to the realization that I needed to inform you about the OpenGL prerequisites. Because I want the framework to be as modern and well-designed as possible, you’ll need… OpenGL 4.5. The latest version, indeed. You might also need an extension, ARB_bindless_texture. That would enable the framework to pass textures to shader in a very stateless way, which is our objective!
I’ll let you know what I decide about that. I don’t want to use an extension that is not implemented almost everywhere.
Well, tests! I need to be sure everything is correctly done on the GPU side, especially the vertex format specification. I’m pretty confident though.
Once the vertex arrays are tested, I’ll start defining a render interface as stateless as I can. As always, I’ll keep you informed!
]]>Before starting up, if you don’t know what hop.nvim is, here’s a small excerpt from the README:
Hop is an EasyMotion-like plugin allowing you to jump anywhere in a document with as few keystrokes as possible. It does so by annotating text in your buffer with hints, short string sequences for which each character represents a key to type to jump to the annotated text. Most of the time, those sequences’ lengths will be between 1 to 3 characters, making every jump target in your document reachable in a few keystrokes.
Today, I want to talk about something at the core of the design of Hop: permutations, and a new algorithm I implemented to (greatly) optimize permutations in Hop.
Disclaimer: it might be possible that the algorithm described here has an official name, as I’m using a well-known data structure, traversing in a way that is also well established. Even though I came up with it, I don’t really know whether it has a name so if you recognize it, or think it is similar to something, feel free to tell me!
When you run a Hop command (whether as a Vim command, such as :HopWord, or from Lua, like :lua require'hop'.hint_words()), Hop is going to place labels, called hints, in your window, over your buffer,
describing sequences of keys to type to jump to the annotated location in the buffer. Those key sequences are generated
as permutations of your input key set, of different lengths. Your input key set is basically the set of keys you
expect to type to hop around. For instance, the default key set is made for QWERTY and is:
asdghklqwertyuiopzxcvbnmfj
The way Hop works is taking those keys and generating permutations of increasing length. The goal is to obviously type
as less as possible (remember: Neovim motion on speed!), so we want to generate 1-sequence permutations first, such as
a, s, d, etc., then switch to 2-sequence permutations, e.g. aj, kn, etc. The most important part of the work
done by Hop is to generate those permutations in a smart way.
Until today, I had implemented a single algorithm doing this. Let’s describe it so that I can introduce the actual topic of this article.
The first algorithm used to generate permutations in Hop was designed around the idea of being able to generate permutations in a co-recursive way (even though it’s not using co-recursion in Lua): given a permutation, you can generate the next one by running the algorithm on the permutation. Doing that over and over generates more permutations. You can sum it up like this:
let first_perm = next_perm({})
let second_perm = next_perm(first_perm)
let third_perm = next_perm(second_perm)
-- etc. etc. stop at a given condition and return all the permutations
In functional programming languages, we call that kind of co-recursion an unfold. We stop building / generating when meeting a condition. In our case, the condition is when we reach the number of permutations to generate. This first algorithm had several constraints I wanted satisfied:
We already saw 1., so let’s talk about the three remaining points. Taking into account the key set of the user, with this algorithm, is done by splitting it into two sub-sets:
abcde, e is a
terminal key.abcde, a, b, c and d all appear before e, so they are sequence
keys.How to know which key is terminal and which is sequence? Well, I use a simple parameter for that, that can be modified
in the user configuration of Hop: term_seq_bias (:help hop-config-term_seq_bias if you are interested). It is a
floating number parameter specifying the ratio of terminal vs. sequence keys to use. Imagine you have 100 keys in your
key set (you typically will have between 20 and 30), setting this parameter to 3 / 4 makes it use 75 terminal keys and
25 sequence keys.
Then, given those two sets, we start from the 0-sequence permutation, a.k.a. {}, and we start using terminal keys. If
we have abc as terminal keys and de as sequence keys, we will get the permutations 'a', 'b' and 'c' for
n = 3. If we ask permutations for n = 4, we will get 'a', 'b', 'c' but not 'd', as it’s a sequence key.
Instead, we will start a new layer by incrementing the dimension of sequences, yielding 2-sequence permutations: we will
then generate 'da'. For n = 6, the last permutation in the list is 'dc', which means that for n = 7, we have to
do something, as we have run out of terminal keys. Instead of starting a new layer (3-sequence permutations), we
traverse the permutation in reverse order, checking that we have exhausted all the sequence keys. Here, we can see that
'd' is not the last sequence key, so we can use the next one, e, and start a new sequence at ea, then eb, ec…
and guess what permutation is the next? Since we have run out of both terminal keys and sequence keys, we need to use
3-sequence keys: the next permutation will be dda.
This algorithm is interesting because it allows people to bias (hence the name) the distribution of terminal and
sequence keys. However, it is pretty obvious that this algorithm does a pretty poor job at minimizing the overall number
of keys to type: it will minimize the number of keys to type for short hops, mostly around the cursor and at
mid-distance. I highly advise to use either 3 / 4 or even 0.5 for the bias value. Other values yield… weird results.
Very quickly, I have noticed something a bit annoying with Hop. Even though it is pretty fast, the distribution of keys
for long jumps is often using 3-sequence permutations. And where does it make sense to use Hop the most? For mid to long
range jumps. Vim and Neovim are already pretty good at short jumps. For instance, jumping to characters on the same line
can be achieved with f and F. If you want to jump to a word on the line above, you can just press
I don’t necessarily agree that those are always ideal (and, well, I do use Hop for short hops too), but the situation is not ideal for long jumps in Vim / Neovim. See, I’ve been using Vim and Neovim for more than 12 years now, and I’ve seen lots of people using it. Most of the time, what people use for long jumps is either (or a combination of) one of these:
relativenumber and look at relative numbers to jump to the line where the location appears in, press
something like 17k or 17j, then use the f / F motions.:153 or 153gg to jump to line 153, and do the same as the previous
point.Among all those options, even when you combine them, you will always have to type more keys / take more time to do
long jumps. For instance, considering we can jump with f to our location l because it is unique on the line we
want to go to, assuming it’s on line 1064, 45 lines above our cursor:
relativenumber, we have to press 45kfl: between 5 and 6 keys (depending on whether you have to press shift on
your keyboard layout for the digits).1064ggfl, between 8 and 9 keys.So clearly, Hop should help for those jumps as much as possible, and the previous algorithm, even though already an enhancement over typing relative numbers or real line numbers, can still be enhanced. It is already pretty good because it will provide you with, most of the time, between 1-sequence and 3-sequence permutations. For long jumps, assume the longest: 3 keys, plus one key to trigger the Hop mode, you get 4 keys to type at most to jump anywhere.
The goal is to reduce that to 2 keys, i.e. 2-sequence at most, most of the time — 3 keys if you count the key to start
Hop. In order to understand the concept of this new algorithm, let’s focus on what was wrong with the former. Consider
the following, using abc as terminal keys and de as sequence keys:
-- n = 3
a b c
-- n = 4
-- last 1-sequence
-- v
a b c da
-- n = 6
a b c da db dc
-- n = 9
a b c da db dc ea eb ec
-- n = 10
-- last 2-sequence
-- v
a b c da db dc ea eb ec dda

You can see that after only 9 words, we are already using 3-sequence permutations. What about, for instance, the following sequences:
aa ab ac bb dd de …
Obviously, because of the rules explained above about the difference between terminal and sequence keys, those are
forbidden: indeed, they would yield ambiguous sequences. For instance, aa and a both start with the same key and
one is terminal at a given depth (1-sequence) while the other still has one level (2-sequence): what should we do?
Trigger the 1-sequence and never be able to jump to the 2-sequence? Use a timeout so that we have the time to type the
second a? But that would make jumping to terminal keys feel delayed / slow, so that’s a hard no. What’s the
problem?
The problem is that we are not efficiently using the key space by forbidding those sequences. If you pay attention, you
should actually be able to use all possible 2-sequence permutations. But in order to do that… we have to forbid using
1-sequence permutations! Forbidding that will make all 2-sequence permutations unique and unambiguous. The difference is
massive: instead of having only 9 permutations shorter than 3-sequence ones, we know have 25, which is the number of
keys in the user key set squared: 'abcde' is 5 keys, and 5² is 25. For the default key set for QWERTY, which has 26
keys, it means 26² = 676 maximum 2-sequence permutations, which is a comfortable number for the visible part of your
buffer.
The new algorithm’s idea relies on using all the keys from your input key set. It doesn’t split it into terminal and
sequence keys. It creates tries (a type of search tree, optimized for packed storage and fast traversal) to store the
permutations and backtracks to remove ambiguity as we ask for more permutations. For instance, using the user input key
set 'abcde':
-- n = 5
a b c d e
-- n = 6
-- e was transformed to ea
-- v
a b
a b c d e e
-- n = 7
a b c
a b c d e e e
-- n = 9
a b c d e
a b c d e e e e e
As you can see, for n = 6, instead of adding a new layer, the algorithm backtracks to fill what is possible to fill:
in our case, e, by replacing it with ea and inserting the new permutation at eb. Once a given layer is saturated,
the algoritm backtracks again, trying to saturate another layer:
-- n = 10
-- d was transformed to da
-- v
a b a b c d e
a b c d d e e e e e
-- n = 13
a b c d e a b c d e
a b c d d d d d e e e e e
-- n = 17
a b c d e a b c d e a b c d e
a b c c c c c d d d d d e e e e e
-- n = 21
a b c d e a b c d e a b c d e a b c d e
a b b b b b c c c c c d d d d d e e e e e
-- n = 25
a b c d e a b c d e a b c d e a b c d e a b c d e
a a a a a b b b b b c c c c c d d d d d e e e e e
Here, we have completely saturated the 2-sequence permutations, yielding 25 of them, and cannot backtrack anymore. When backtracking fails, we simply augment the last trie, deepest:
-- n = 26
a b
a b c d e a b c d e a b c d e a b c d e a b c d e e
a a a a a b b b b b c c c c c d d d d d e e e e e e
And we go on. Here, you can see a repetition of the same key, such as e e e e, but because those are tries, they are
going to be actually encoded as a single node, having several children. Consider this example:
-- encode these permutations
--
-- a b c d
-- a b c d e e e e
local trie = {
{
key = 'a';
trie = {}
},
{
key = 'b';
trie = {}
},
{
key = 'c';
trie = {}
},
{
key = 'd';
trie = {}
},
{
key = 'e';
trie = {
{ key = 'a'; trie = {} },
{ key = 'b'; trie = {} },
{ key = 'c'; trie = {} },
{ key = 'd'; trie = {} },
}
}
}
Two interesting properties about this algorithm:

This new algorithm was a lot of fun to come up with, design, test, implement and eventually use, because yes, it is
already available (as a default) in hop.nvim. If for any reason you would prefer to use the first one, you can still
change that in the user configuration (see :h hop-config-perm_method). Another point, too: since the support for the
setup workflow, it’s been possible to locally override Lua function calls opts argument. You can then make a key
binding using the first algorithm and another one using the new one – all this is explained in the vim help page 😉.
It’s up to you!
Keep the vibes!
]]>This problem doesn’t look too hard at the first glance, but has some hidden complexities than I thought pretty interesting. Just to sum up if you don’t want to open the link, the first part requires you to parse a hexadecimal number and interpret its binary representation as a packet, like what you would find in a network packet format, basically. You need to be able to parse correctly a single packet, which can contain nested packets. In this puzzle, packets represent expressions, that can either be:
Each packet have a header, comprising a header and its type (literal or operator), and then the actual data of the packet.
From this context, it seems pretty obvious to assume this is going to be a parsing problem. The first part asks you to parse the packet and its nested packets, and add all the version numbers of the headers of all packets. The format goes as this (quoting):
Every packet begins with a standard header: the first three bits encode the packet
version, and the next three bits encode the packettype ID. These two values are numbers; all numbers encoded in any packet are represented as binary with the most significant bit first. For example, a version encoded as the binary sequence 100 represents the number 4.
Then, you have the description of a packet representing a literal number:
Packets with
type ID 4represent a literal value. Literal value packets encode a single binary number. To do this, the binary number is padded with leading zeroes until its length is a multiple of four bits, and then it is broken into groups of four bits. Each group is prefixed by a 1 bit except the last group, which is prefixed by a 0 bit. These groups of five bits immediately follow the packet header. For example, the hexadecimal stringD2FE28becomes:
110100101111111000101000
VVVTTTAAAAABBBBBCCCCC
Below each bit is a label indicating its purpose:
- The three bits labeled
V(110) are the packetversion,6.- The three bits labeled
T(100) are the packettype ID,4, which means the packet is a literal value.- The five bits labeled
A(10111) start with a1(not the last group, keep reading) and contain the first four bits of the number,0111.- The five bits labeled
B(11110) start with a1(not the last group, keep reading) and contain four more bits of the number,1110.- The five bits labeled
C(00101) start with a0(last group, end of packet) and contain the last four bits of the number,0101.- The three unlabeled
0bits at the end are extra due to the hexadecimal representation and should be ignored.So, this packet represents a literal value with binary representation
011111100101, which is2021in decimal.
I’m just going to focus on this first kind of packet because it’s already interesting.
From this description, if you have ever done bit parsing, you should already know that there is going to be a problem. You can parse a literal packet using this kind of pseudo code (ignoring errors for simplicity, it’s AoC!):
fn parse_packet(&mut self) -> Packet {
let version = self.next_bits(3);
let type_id = self.next_bits(3);
match type_id {
4 => todo!("literal parser"),
_ => todo!("operator parser"),
}
}
The big question is: how do we implement next_bits? Getting 3 bits of information? As you might know, the smallest
amount of information a computer can manipulate is a byte, which is 8 bits, so… how do we manipulate less than that?
I would like to say one thing before going on. The input is a hexadecimal string. The first step of your solution is to
extract the binary representation. For instance, A (10 in decimal) is encoded as 1010 and 5A is encoded as
01011010. As you can see (might already know), hexadecimal is easy to parse into bytes: group hexadecimal digits by
two, convert each digit to 4 bits, and glue them together. 5A is 5 -> 0101 and A -> 1010, which gives 01011010
as seen above.
But as we have seen with next_bits above, we will want to manipulate bits, not bytes. So you have several choices and
design decisions to make here:
You can go the « easy » way and store each bit as a byte. This is very suboptimal, because you are going to waste 8
times the amount of memory you actually need to store all this. For instance, 5A (01011010), which is a single byte
on your computer, will require 8 bytes to be stored: 00000000, 00000001, 00000000, 00000001, 00000001, 00000000, 00000001, 00000000. That is very inefficient, but it would work, if you put them in a Vec<u8>. To do that, you would
basically need to read one hexadecimal digit at a time (which has 4 bits), and then perform bitwise right shifts with
masking (& 0x1) to extract the bit. Something like:
fn inefficient_bit_extract(&mut self, four_bits: u8) {
for i in 0..4 {
self.bits.push((four_bits >> i) & 0x1);
}
}
With this solution, implementing next_bits is easier, because you only need to pop the Vec<u8>. Let’s write
next_bits with this kind of implementation. Because we do not need more than 15 bits in this puzzle (explained later in
the puzzle text, not really important for now), we will return the read bits on u16:
fn next_bits(&mut self, n: usize) -> u16 {
assert!(n < 16);
let mut out = 0;
for i in 0..n {
out = (out << i) | self.bits.pop() as u16;
}
out
}
Let’s explain quickly.
out = (out << i) | self.bits.pop() as u16;
This will extract a single bit, like 00000001, will convert it to 16-bit (so 0000000000000001) and will OR it to
the number we are building. That number is left shifted at each iteration so that we only OR the least significant bit
at each iteration. For instance, if we call next_bits(3) and we get 101, this implementation will basically store
this in out at each iteration:
0000000000000001.0000000000000010.0000000000000101.Easy, and works.
However, I am a perfectionist. This is not satisfying to me, and it has been years since I have been wanting to
implement something like what I am about to describe. I have been thinking about this problem with a different context
but it is similar: a bool value, in a computer, is stored on the minimal amount of data a computer can manipulate:
a byte. It means than storing a boolean value wastes 7 bits. If you have a single boolean value, there is nothing you
can do. But if you need several boolean values, like, six booleans… using 6 bytes still wastes 42 bits for 6 bits of
useful data! And 6 bits can hold in a single byte. So we should be able to store booleans in a packed array of bytes.
The idea is the following: imagine that you want to store six boolean value in a byte:
00100101
ABCDEFGH
I have labelled each bit with a letter to make it easier to reference them. If you want to, for instance, the boolean
value stored at index 2 (F in our representation), all you have to do is to right shift that number using the index of
the element and 1-bit mask:
fn read_boolean(byte: u8, index: usize) -> u8 {
assert!(index < 8);
(byte >> index) & 0x1
}
If you want to set it, you need to do the opposite operation using a logical OR:
fn set_boolean(byte: &mut u8, index: usize) {
*byte = *byte | (0x1 << index);
}
I wanted to use this AoC 18 day to encode my solution as a packed array of bits, and still have the next_bits
interface that would allow me to get between 0 and 15 bits of data, encoded as u16. The catch is: because I bit pack
the hexadecimal number, it is going to lie in a Vec<u8>. And the whole complexity relies in the fact asking for N
bits can span across several bytes in the packed array…
The first thing we need to do is to think about the interface. I want that mutable next_bits interface. What it means
is that I want to be able to read as many bits as I want (given less than 16 bits), I should not have to worry about the
byte implications behind the scenes. The puzzle text clearly shows that when we are going to parse a literal value, we
will have to read groups of 5 bits. Reading two literal numbers already implies two bytes (10 bits), and even the first
group implies reading two bytes, because before that, you have the header on 6 bits (3 bits for the version, 3 bits for
the type ID), living 2 bits to read for the beginning of the group, and 3 bits in the next byte to read. That’s a lot of
complexity that should be hidden from the interface.
Let’s start with the Decoder type:
#[derive(Clone, Debug)]
struct Decoder {
bytes: Vec<u8>,
bitpos: usize, // the position in bit in the byte array
}
This is fairly simple: bytes is the bit-packed array I mentioned, and bitpos is the current position in that array.
But it’s not the position in the array elements, it’s the position in bits. If bytes has 3 bytes (24 bits), then
bitpos can be between 0 and 23. To know in which actual array element the bit is, we simply need to divide by 8.
For instance, if bitpos = 17, then 17 / 8 = 2 tells us that the bit is in bytes[2]. To know the actual bit
position in that byte, you will have guessed it, you need to take the modulo: 17 % 8 = 1, which is the second most
significant bit. You can convert to least significant bit by subtracting that number to 8: 8 - 1 = 7.
With all that information, let’s fill the bytes:
impl Decoder {
fn new(input: &str) -> Self {
let digits: Vec<_> = input.chars().flat_map(|hexa| hexa.to_digit(16)).collect();
let bytes = digits.chunks(2).map(|d| (d[0] << 4 | d[1]) as u8).collect();
Self { bytes, bitpos: 0 }
}
Here, because this is AoC, I don’t care about errors, and hence use flat_map to remove Option or Result in
iterators. The important thing here is that I group hexadecimal digits by 2 (the .chunks(2) part), and then simply do
this:
|d| (d[0] << 4 | d[1]) as u8
d[0] are the most significant 4 bits, so I left shift them by 4 bits, and d[1] is already correctly positioned, so
nothing to do with it. I convert to u8 parse to_digit(16) yields u32.
Then, let’s implement next_bits.
fn next_bits(&mut self, n: usize) -> u16 {
// how many bits are still available from the current byte
//
// 0000000011111111
// ^ position is 5, so we have 3 bits still available
let bits_avail = 8 - self.bitpos % 8;
if bits_avail >= n {
// we have enough in the current byte; compute the new position and right shift to align correctly
let shift = bits_avail - n;
let byte = (self.bytes[self.bitpos / 8] as u16 >> shift) & ((1 << n) - 1);
self.bitpos += n;
byte
} else {
// …
}
The first step to do is to check whether the amount of bits requested would make us overlap with another byte given the
current bitpos. If it’s not the case, then the problem is trivial. As shown in the example, we can just simply right
shift by the number of available bits minus the requested amount (because we might still have room afterwards), and mask
the N least significant bits. By the way, this is a funny one: setting to 1 the N significant bits. If you have N = 3,
you want (on 8-bit here to simplify) 00000111. How do you make that quickly? It’s actually quite simple: what is this
number? This 111? It’s an odd number (because its least significant bit is 1) and it looks like it’s almost 2³. It’s
actually 2³ - 1. And this makes sense: remember in school / when you were learning binary that the biggest number you
can represent on N bits is 2^n - 1. Like, on 8-bit, 2⁸ is 256, and the biggest number you can represent is 255, as you
might already know, which is 11111111. So, we can simply deduce 00111…1 is the biggest number for 2^N where N is the
number of 1. And that number is 2^N - 1. A power of 2, in binary, is a simple shift. So, 2^N is 1 << N. And `2^N
is…(1 << N) - 1`. You have it.We then increment bitpos by the number of read bits, and return the actual byte, for which the read bits are right
shifted.
Now, what happens when bits_avail < n? We have to read bits_avail and shift them in the 16-bit output. Then we will
have to switch to the next byte, and read n - bits_avail additional bits, because n is the requested number of bits
to read and since we read every bits available from the current bytes, well, n - bits_avail. The « byte switching »
logic can be a bit overwhelming at first but is actually not that hard: once we have read bits_avail, we know that the
next byte will have 8 bits available. We can then divide the problem in two sub-problems, easier:
n >= 8.This logic is implemented in the } else { branch, so I will just copy the else and put the code, and explain it:
} else {
// this is the annoying part; first, get all the bits from the current byte
let byte0 = (self.bytes[self.bitpos / 8] as u16) & ((1 << bits_avail) - 1);
self.bitpos += bits_avail;
// then, left shift those bits as most significant bits for the final number
let mut final_byte = byte0;
// then, we need to get the remaining bits; compute the number of bytes we need
let n = n - bits_avail;
let bytes_needed = n / 8;
// while we require more than 1 byte, the problem is trivial; just copy and left shift the bytes
for _ in 1..=bytes_needed {
let byte = self.bytes[self.bitpos / 8] as u16;
self.bitpos += 8;
final_byte = (final_byte << 8) | byte;
}
// the remaining bits can be extracted from the last byte
let n = n % 8;
let shift = 8 - n;
let byte = (self.bytes[self.bitpos / 8] as u16 >> shift) & ((1 << n) - 1);
self.bitpos += n;
final_byte = (final_byte << n) | byte;
final_byte
}
}
Let’s dive in.
let byte0 = (self.bytes[self.bitpos / 8] as u16) & ((1 << bits_avail) - 1);
self.bitpos += bits_avail;
This is the current byte, from which, as said earlier, we need to read all available bits. Because we are going to read
everything from the current position to the least significant bit, we can just read the whole byte and apply a mask for
the bits_avail least significant bits (remember the explanation earlier). So & ((1 << bits_avail) - 1) does the job.
Because we have read bits_avail bits, we increment bitpos as well.
// then, left shift those bits as most significant bits for the final number
let mut final_byte = byte0;
This part is not strictly needed as I could have called byte0 final_byte in the first place, but I wanted a clear
mind about what was doing what. The idea for the following steps is that we are going to accumulate bits in final_byte
by left shifting it before applying the logical OR. The amount of shift to apply depends on the amount of bits read.
For the case where we read a whole byte, we will just right shift by 8 bits.
// then, we need to get the remaining bits; compute the number of bytes we need
let n = n - bits_avail;
let bytes_needed = n / 8;
This is a pretty straight-forward code given what I said above. n - bits_avail is the remaining amount of bits to
read. bytes_needed is the number of full bytes we will have to go through (in self.bytes), and from which we will
extract 8 bits each. The rest then makes more sense:
// while we require more than 1 byte, the problem is trivial; just copy and left shift the bytes
for _ in 1..=bytes_needed {
let byte = self.bytes[self.bitpos / 8] as u16;
self.bitpos += 8;
final_byte = (final_byte << 8) | byte;
}
Then, the remaining bits are the last part of the parsing problem. The idea is that we know we will have to read between
1 and 7 bits from that byte. That information is stored as n and shift:
let n = n % 8;
let shift = 8 - n;
We then extract the information, and return the N-bit number:
let byte = (self.bytes[self.bitpos / 8] as u16 >> shift) & ((1 << n) - 1);
self.bitpos += n;
final_byte = (final_byte << n) | byte;
final_byte
Armed with next_bits, the problem really becomes trivial. Let’s decode a literal packet and introduce the Packet
type:
#[derive(Clone, Debug, Eq, PartialEq)]
struct Header {
version: u8,
type_id: u8,
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum Packet {
Literal {
header: Header,
value: u128,
},
Operator {
header: Header,
length_type_id: u8,
packets: Vec<Packet>,
},
}
And Decoder::decode:
impl Decoder {
// …
fn decode(&mut self) -> Packet {
let version = self.next_bits(3) as u8;
let type_id = self.next_bits(3) as u8;
let header = Header { version, type_id };
match type_id {
// literal value
4 => {
let mut value = 0u128;
loop {
let bits = self.next_bits(5) as u128;
value = (value << 4) | (bits & 0xF);
if (bits >> 4) & 0x1 == 0 {
break Packet::Literal { header, value };
}
}
}
// operator
_ => {
todo!()
}
}
}
}
Some explanation. Given the format, we know that a literal number is made of packets of 5 bits, where the most
significant bit does not belong to the number, but instead is a flag. If set to 0, it means that the literal is over.
value = (value << 4) | (bits & 0xF);
This accumulates the 4 least significant bits of the 5 bits into the actual literal number.
if (bits >> 4) & 0x1 == 0 {
And this performs the test on the most significant bit to check whether the literal number is fully built.
Operators represent the other kind of packets, and are introduced, quoting:
Every other type of packet (any packet with a type ID other than 4) represent an operator that performs some calculation on one or more sub-packets contained within. Right now, the specific operations aren’t important; focus on parsing the hierarchy of sub-packets.
An operator packet contains one or more packets. To indicate which subsequent binary data represents its sub-packets, an operator packet can use one of two modes indicated by the bit immediately after the packet header; this is called the length type ID:
- If the length
type IDis0, then the next15bits are a number that represents the total length in bits of the sub-packets contained by this packet.- If the length
type IDis1, then the next11bits are a number that represents the number of sub-packets immediately contained by this packet.Finally, after the
length type IDbit and the 15-bit or 11-bit field, the sub-packets appear.
There is nothing else to do than just applying those rules:
// in Decoder::decode’s match on type ID
// operator
_ => {
let length_type_id = self.next_bits(1) as u8;
let mut packets = Vec::new();
if length_type_id == 0 {
let total_length_bits = self.next_bits(15) as usize;
let mut bits_read = 0;
while bits_read < total_length_bits {
let bitpos = self.bitpos;
packets.push(self.decode());
bits_read += self.bitpos - bitpos;
}
} else {
let sub_packets_nb = self.next_bits(11);
for _ in 0..sub_packets_nb {
packets.push(self.decode());
}
}
Packet::Operator {
header,
length_type_id,
packets,
}
}
Because of the next_bits interface, for when length_type_id == 0, we have no way to know deterministically when to
stop, because we don’t really know how long each packet is. Because of this, the bits_read variable is introduced so
that we know how many bits we read. That inforation is easy to get by comparing the value of bitpos before and after
reading a packet. For the other branch, there is nothing interesting to comment here.
The first question was:
Decode the structure of your hexadecimal-encoded BITS transmission; what do you get if you add up the version numbers in all packets?
And my input was (it was a single string but I break it on several lines, because it’s pretty long):
60552F100693298A9EF0039D24B129BA56D67282E600A4B5857002439CE580E5E5AEF67803600D2E294B2FCE8AC489BAEF37FEACB31A678548034EA0
086253B183F4F6BDDE864B13CBCFBC4C10066508E3F4B4B9965300470026E92DC2960691F7F3AB32CBE834C01A9B7A933E9D241003A520DF31664700
2E57C1331DFCE16A249802DA009CAD2117993CD2A253B33C8BA00277180390F60E45D30062354598AA4008641A8710FCC01492FB75004850EE5210AC
EF68DE2A327B12500327D848028ED0046661A209986896041802DA0098002131621842300043E3C4168B12BCB6835C00B6033F480C493003C4008002
9F1400B70039808AC30024C009500208064C601674804E870025003AA400BED8024900066272D7A7F56A8FB0044B272B7C0E6F2392E3460094FAA500
2512957B98717004A4779DAECC7E9188AB008B93B7B86CB5E47B2B48D7CAD3328FB76B40465243C8018F49CA561C979C182723D76964220041275627
1FC80460A00CC0401D8211A2270803D10A1645B947B3004A4BA55801494BC330A5BB6E28CCE60BE6012CB2A4A854A13CD34880572523898C7EDE1A9F
A7EED53F1F38CD418080461B00440010A845152360803F0FA38C7798413005E4FB102D004E6492649CC017F004A448A44826AB9BFAB5E0AA8053306B
0CE4D324BB2149ADDA2904028600021909E0AC7F0004221FC36826200FC3C8EB10940109DED1960CCE9A1008C731CB4FD0B8BD004872BC8C3A432BC8
C3A4240231CF1C78028200F41485F100001098EB1F234900505224328612AF33A97367EA00CC4585F315073004E4C2B003530004363847889E200C45
985F140C010A005565FD3F06C249F9E3BC8280804B234CA3C962E1F1C64ADED77D10C3002669A0C0109FB47D9EC58BC01391873141197DCBCEA401E2
CE80D0052331E95F373798F4AF9B998802D3B64C9AB6617080
So:
Packet.version field of all sub-packets, summing them.fn solve1(input: &str) -> u32 {
let mut decoder = Decoder::new(input);
checksum(&decoder.decode())
}
fn checksum(packet: &Packet) -> u32 {
match packet {
Packet::Literal { header, .. } => header.version as _,
Packet::Operator {
header, packets, ..
} => header.version as u32 + packets.iter().map(checksum).sum::<u32>(),
}
}
Yes, it’s that simple once you have made the right framework. :) Note that this function is not tail-recursive. A tail-recursive version would probably perform faster, but it was so convenient to write and took me 15s.
Part 2 explains that the operator packets are actually expression operators, operating on literal values. Basically, it’s a whole AST. The rules are:
type ID 0 are sum packets - their value is the sum of the values of their sub-packets. If they only
have a single sub-packet, their value is the value of the sub-packet.type ID 1 are product packets - their value is the result of multiplying together the values of their
sub-packets. If they only have a single sub-packet, their value is the value of the sub-packet.type ID 2 are minimum packets - their value is the minimum of the values of their sub-packets.type ID 3 are maximum packets - their value is the maximum of the values of their sub-packets.type ID 5 are greater than packets - their value is 1 if the value of the first sub-packet is greater
than the value of the second sub-packet; otherwise, their value is 0. These packets always have exactly two
sub-packets.type ID 6 are less than packets - their value is 1 if the value of the first sub-packet is less than
the value of the second sub-packet; otherwise, their value is 0. These packets always have exactly two sub-packets.type ID 7 are equal to packets - their value is 1 if the value of the first sub-packet is equal to the
value of the second sub-packet; otherwise, their value is 0. These packets always have exactly two sub-packets.This is my solution:
fn solve2(input: &str) -> u64 {
let mut decoder = Decoder::new(input);
eval(&decoder.decode())
}
fn eval(packet: &Packet) -> u64 {
match packet {
Packet::Literal { value, .. } => *value as _,
Packet::Operator {
header, packets, ..
} => match header.type_id {
0 => packets.iter().map(eval).sum::<u64>(),
1 => packets.iter().map(eval).product::<u64>(),
2 => packets.iter().map(eval).min().unwrap(),
3 => packets.iter().map(eval).max().unwrap(),
5 => {
if eval(&packets[0]) > eval(&packets[1]) {
1
} else {
0
}
}
6 => {
if eval(&packets[0]) < eval(&packets[1]) {
1
} else {
0
}
}
7 => {
if eval(&packets[0]) == eval(&packets[1]) {
1
} else {
0
}
}
id => panic!("unknown type id: {}", id),
},
}
}
And here you have it all. The complete solution is available on my GitHub repository for AoC 21. I don’t think it was the hardest problem, nor the more interesting, but the decision (and challenge) I decided to go with (bit-packing with a bit-driven interface) made the whole thing much more fun and interesting to me.
I hoped you liked it and that you learned something, especially regarding bitwise operations. That was pretty heavy! I
think that you should also now have an idea about how you could write a specialized version of Vec<bool> that
bit-packs booleans value. Obviously, we do not want Vec<bool> to actually do that kind of specialization behind the
scenes (think FFI: when getting a *const bool or *mut bool from the Vec<bool>, if those pointers point to
bit-packed data… it’s going to be a nightmare if the FFI function you use expect an actual array of unpacked boolean
values).
For the rest of AoC, I’m probably going to continue working on it but I’m currently pausing at day 18 (Christmas, fatigue, a bit of OSS burnout too). I will probably write some more about AoC once I feel rested.
Have fun and keep the vibes!
]]>Today I released a new version of warmy, the warmy-0.6.0 release. That release kicks in with a
few additions, among:
That bug caused long-lasting reloads on stream-copied resources go into weird behavior.
In order to get the bug, I must give a bit of context.
Imagine you have a large resource, like a 4K texture or a big mesh. Whenever your favourite
software writes it to disk, it’s very likely it’ll stream chunks by chunks of bytes. For
instance, it might choose to copy the resource 2 MB by 2 MB on disk. On each copy, your
file system will generate WRITE events, that warmy will intercept. Before warmy-0.6.0, the
default behavior was to reload the resource on the first WRITE event, which was already
wrong, because only a very small part of the resource would have changed – I actually witnessed
weird behaviors with textures in a demo of mine I’m working on, not seeing the new textures in my
demo.
But there’s worse. There’s a parameter you can set of your Store, called update_await_time_ms.
That parameter gives warmy a hint about how much time must have passed since the last update in
order to effectively call the Load::reload function. However, this is a bit twisted, because if
the resource takes more time to reload than update_await_time_ms, it’ll get repeatedly loaded –
for as many as WRITE events were generated. This is a bit sick, yeah.
The fix was pretty simple: change the semantics of that update_await_time_ms. In 0.5.2, it has
the default value of 1s, meaning that a resource wouldn’t reload if it was reloaded less than a
second ago. The new semantics works on the future. Whenever a WRITE event is intercepted, warmy
will call the Load::reload function only if no WRITE event is intercepted in the next
update_await_time_ms. It’s a bit like the implementation of a click and a double click: you must
wait a bit after you got a MouseRelease event in order to interpret is as a Click because
another MouseRelease could arrive soon (generating a DoubleClick if it’s soon enough).
You’ll also notice that
update_await_time_msname sticks better to the new semantics!
In 0.5.2, the default value for update_await_time_ms was 1s. If we kept that value, it would
result in a pretty bad overall latency. The value was lowered to 50ms instead.
You can still tweak that value if it doesn’t suit your needs.
More information can be found in the changelog.
I’ve been very happy with what warmy has brought to me so far. Other people also gave it a try
and for now seem to enjoy it. I’ve gathered a few ideas for the future, based on IRL talks over
a beer several beers, and GitHub issues / pull requests:
Future.Feel free to test it, and as always, keep the vibes!
]]>1.0 release for months now and came to a very important realization: lots of changes have been
done and I’m still not ready to release it. I’ve kind of lost myself in the process.
This article is about two main topics:
If you don’t care about the changes, feel free to go read this to know how you can learn about luminance and graphics programming in general.
Lately, luminance has received a wide range of feature additions, bug fixes and design ideas. I’ve
been wanting to release all those changes as part of the 1.0 release but the thing is: among all
the changes, some have been around on the master branch for months without a proper release on
crates.io… because the 1.0 milestone was not reached. People have been moving towards luminance
more and more and some provided feedback about using luminance. Happy but indecisive about what to
do, I faced a dilemma:
1.0 but eventually block people from using all
the cool features of luminance because the last release on crates.io is months old.1.0 for everything and 0.31 for all the new
candies.I have decided to go with the second option.
Just before writing this article, the last luminance version was 0.30 — just for the record,
that version is eleven months old, ~200 commits behind master. The new and most recent version is
thus 0.31 and crates got updated:
#[derive(..)]
construct.Lot of work has been accomplished and I received several contributions from people all around the globe, including PRs and issues. I’d like to remind that I appreciate both and you are really encouraged to contribute in any way you want to. Special thanks fly to:
About that last point, my Reddit and Twitter interactions about luminance have been very interesting because I got to test the water about how people feel about luminance, especially when compared to (not so) similar crates, such as [gfx], glium or even gl. I came to the realization, after reading people and what they would love to have as a graphics crate, that luminance can have the role of the easy crate, that is not necessarily the fastest but fast enough to be quickly productive. I cannot help it but keep thinking about a simple question: if some people can make games in Java or C# with a garbage collector or using old tech like OpenGL 2.1 (yes, some people still make pretty good games with that), why would one need a perfect zero-cost abstraction down-to-the-metal unsafe ultra-optimized crate to write something? See, I went round and squares about that topic, because I’ve already used luminance in several projects of mine (mostly demoscene purposes), and it just does the job. So, yes, luminance doesn’t have that cool, new and modern Vulkan-type API, I must confess. But it has its own API, which is very functional-based (see the History section of luminance for further details about that) and, to me, modern enough so that people don’t get frustrated with the overall design being too clunky.
So, yeah. I gave up on the idea of introducing backends in luminance. I really changed my mind several times about that topic and it’s actually when I read comments on Reddit about people getting confused about the direction of the crate that I made up my mind: luminance must remain simple. Having a system of backends that can be hot-switched, concurrent etc. is just going to make things hard for people to use it — and maintain it! It’s likely that I will introduce, however, feature-gates to allow to compile luminance on WebAssembly via WebGL, OpenGL ES or even Vulkan at some point, but the difference is that no backend will be implemented. That means that those feature-flags aren’t likely to be summable at first. But all of this will be done in future releases; stay tuned.
The current blog post brings a description of all the changes of luminance-0.31. It should be the
last 0.*.* version before hitting the 1.0 release. To the question:
Why not releasing
1.0directly?
I answer that the 1.0 milestone has a backlog with two major changes that will take time to
implement and I think it would be a pity to postpone a lot of great changes that are already
available because of two features that are yet to be implemented. The concept of versions is to
allow releasing features in a structured way without having to wait too much. So here we are.
This post also show cases a small tutorial about how to get started with luminance. The very first steps you should have would be to have a look at the examples/ directory and try to play with all of the samples.
Disclaimer: the following section of this article is based on luminance’s changelog.
panic! and attributeless renders.Format::R and Format::RG when querying a texture’s texels.GTup. No code was using it and it was not really elegant.uniform_interface! macro and replace it with the UniformInterface procedural
derive macro.mut operation. That is required to lock-in the mapped slices
and prevent to generate new ones, which would be an undefined behavior in most graphics backends
such as OpenGL.Texture<..> type anymore, as a type family is now used to ease the generation of
color and depth slots).Vertex trait is implemented.
Vertex::vertex_format method has been renamed Vertex::vertex_desc.VertexFormat, that method now returns a VertexDesc. Where a
VertexFormat was a set of VertexComponentFormat, a VertexDesc is a set of
VertexBufferDesc.VertexBufferDesc is a new type that didn’t exist back then in 0.30. It provides new data
and information about how a vertex attribute will be spread in a GPU buffer. Especially, it has:
VertexAttribDesc, the new name of VertexComponentFormat.VertexComponentFormat was renamed VertexAttribDesc.VertexAttribType’s integral variants.VertexAttrib. Such a trait is used to map a type to a
VertexAttribDesc.Vertex has zero implementor instead of several ones in 0.30. The reason for that is that
VertexBufferDesc is application-driven and depends on the vertex semantics in place in the
application or library.Semantics trait.
Implementing directly Semantics is possible, even though not recommended. Basically,
Semantics provides information such as the index and name of a given semantics as long
as the list of all possible semantics, encoded by SemanticsDesc.Vertex and Semantics proc-macro derive in the
[luminance-derive] crate.Tess type to make it easier to work with.
Tess::new and Tess::attributeless functions were removed.TessBuilder type was added and replace both the above function.a ..= b operator, allowing to slice a Tess with inclusive closed bounds...= b operator, allowing to slice a Tess with inclusive bounds open on the left
side.Tess::new associated function expected indices to be a slice of u32. This
new release allows to use any type that implements the TessIndex trait (mapping a type to a
TessIndexType. Currently, you have u8, u16 and u32 available.Tess::{as_index_slice,as_index_slice_mut}. Those now enable you to conditionally slice-map
the index buffer of a Tess, if it exists.SamplerType trait, used as constraint on the
Pixel::SamplerType associated type.Floating texture without caring about the
actual type. That is especially true as you typically use sampler2D in a shader and not
sampler2DRGB32F.layout (location = _) is
correctly set to the right value regarding what you have in your Tess’ vertex buffers. That
was both unsafe and terribly misleading (and not very elegant). The new situation, which
relies on vertex semantics, completely gets rid of vertex locations worries, which get
overrided by luminance when a shader program gets linked.enums — such as DepthTest — variants from Enabled / Disabled to
On / Off or Yes / No, depending on the situation.swap_buffers from GraphicsContext to Surface in [luminance-windowing].GenMipmaps instead of bool to encode whether mipmaps should be generated in
texture code. That change is a readability enhancement when facing texture creation code.Dimensionable::zero_offset() a constant, Dimensionable::ZERO_OFFSET.bool to CursorMode.unsafe can be
implemented in a safe way with that crate, so you should definitely try to use it.#[derive(Vertex)]: derive the Vertex trait for a struct.#[derive(Semantics)]: derive the Semantics trait for an enum.#[derive(UniformInterface)]: derive the UniformInterface trait for a struct.Program<_, _, ()> and still set uniform values by querying the uniforms
dynamically. This feature also fully benefits from the strongly typed interface of Uniform<_>,
so you will get TypeMismatch runtime error if you try to trick the type system.std feature gate, allowing to compile with the standard library – this is enabled by
default. The purpose of this feature is to allow people to use default-features = false to
compile without the standard library. This feature is currently very experimental and shouldn’t
be used in any production releases so far – expect breakage / undefined behaviors as this
feature hasn’t been quite intensively tested yet.R11FG11FB10F pixel format.WindowOpt now has support for multisampling. See the WindowOpt::set_num_samples for
further details.Norm is a normalized pixel format. Such formats state that the
texels are encoded as integers but when fetched from a shader, they are turned into
floating-point number by normalizing them. For instance, when fetching pixels from a texture
encoded with R8UI, you get integers ranging in [0; 255] but when fetching pixels from a
texture encoded with NormR8UI, even though texels are still stored as 8-bit unsigned integers,
when fetched, you get floating-point numbers comprised in [0; 1].std::mem::uninitialized references, as it is now on deprecation path. Fortunately, the
codes that were using that function got patched with safe Rust (!) and/or simpler constructs.
It’s a win-win.#[repr(C)] annotation on vertex types in examples. That is a bit unfortunate because
such an annotation is very likely to be mandatory when sending data to the GPU and it should be
done automatically instead of requiring the user to do it. That situation will be fixed in a
next release.cargo run --example command. Read more
here.#![deny(missing_docs)]. The situation is still
not perfect and patch versions will be released to fix and update the documentation. Step by
step.11-query-texture-texels example, which showcases how to query a texture’s texels and
drop it on the filesystem.irc.freenode.net#luminanceI also spent quite a lot of time and energy working on two ways to learn luminance:
So far, I’m writing articles and the wiki is going to be updated let’s say, every week, if I find enough spare time. You can also tell me what kind of stuff you’d like to learn and do.
The official documentation is up to date but is not as good as I expect it to be. I will be adding patches versions to 0.31 to update it.
I hope you like luminance and will make a good use of it. And of course, keep the vibes!
]]>Actually, it’s from 0.25.2, but I’ll just talk about the latest patched.
That new release received several patches:
gl dependencies was updated to gl-0.10.0.uniform_interface! macro was added.I’ll introce that uniform_interface! macro in this blog entry.
That macro was written to provide a smoother and better experience when writing types that will act
as uniform interfaces in shader. For the record, when you create a Program<_, _, _> with
luminance, the third type variable is called the uniform interface. You’ll be handed back a
value of that type when your shader is being used. That will give you the possibility to send data
to the sader prior to making any rendering.
The main idea is to have a type like this:
struct Common {
resolution: Uniform<[f32; 2]>,
time: Uniform<f32>,
jitter: Uniform<f32>
}
If you build a program which type is Program<_, _, Common>, whenever you ask a pipeline to use
that program, you’ll be handed a Common object so that you can access back the uniforms.
In order for that to happen, though, there’s a constraint: your type must implement a trait. That
trait is UniformInterface. There’s no magic behind that – though, it can be a bit harsh to
implement that trait. If you’re using luminance or plan to, I strongly advise you to try an
impl that trait by hand first.
Anyway, after the fifth or sixth uniform interface you’ll have written, you’ll start to get bored of writing the same code every now and then. That’s the exact same feeling as for serializing and deserializing with serde: it’s interesting to do it by hand the first time, but it gets really, really annoying after a while.
So it’d be great to be able to automatically derive UniformInterface for your own types.
There’re two solutions here:
#[derive(UniformInterface)].macro_rules!.(1) is great because it seems seamless when you use it – you already use
#[derive(Clone, Debug, Eq, PartialEq, Etc)]. However, even though it’s doable to learn syn in
order to parse the Rust token stream, it’ll require some time and patience while I wanted something
quick to mockup the idea. For that, (2) is perfect, because regular macros are easy and if
correctly written, shouldn’t add too much weirdness over the type.
The idea is to move to (1) if I find (2) to be a success and really useful.
So… we’re talking about the uniform_interface! macro. This macro has already a pretty decent
documentation, so feel free to visit it, but I’ll explain more here.
A macro is generally used to introduce an EDSL. For instance, nom’s do_parse macro
uses a funny EDSL (you use operators like >> and return result with (…), which is not
normal Rust code).
The uniform_interface! macro uses an EDSL that you know very well: it’s plain Rust code! Hot
news: you’ll have nothing more to learn!
Note: it’s not really any Rust code, since you can only define
structs. Then, see that as a subset of Rust.
The syntax is very simple. Let’s take back our sample from above with the Common interface but now
let’s the macro do all the magic for us!
#[macro_use]
extern crate luminance;
uniform_interface! {
struct Common {
resolution: [f32; 2],
time: f32,
jitter: f32
}
}
You’ll notice two interesting properties:
Uniform<_>: the macro does it for you.UniformInterface impl is automatically generated for you!The second point is the most interesting one, since it’s akin to writing
#[derive(UniformInterface)]. As you can see, the syntax overhead is not really a problem.
The cool part of that macro is that it also supports Rust annotations on its fields. You have two possible annotations available:
as("something") behaves by not using the field’s direct name and instead use the one provided as
argument.unbound enables not to make the whole uniform interface fail if a field cannot be mapped on
the GPU side.The as(…) annotation is very simple and straight-forward to get, since it lets you change the
binding name of the field. The unbound annotation is a bit trickier and you need to understand how
UniformInterface expects the value to be constructed.
By default, when you try to map a field to something on the GPU side (shader program), you use the
UniformBuilder::ask function. If you have a closer look at function, you can see that it returns
Result<Uniform<T>, UniformWarning>. In order to get the Uniform<T> out, you’re left with the
choice of how you should handle errors. And that will depend on how you want your value to be built.
For instance, some fields might be completely mandatory for your shader to work and others optional.
unbound tags a field as optional.
You can mix both annotations if you want to remap an optional field!
#[macro_use]
extern crate luminance;
uniform_interface! {
// we want to export that, so we also make it pub
pub struct Common {
resolution: [f32; 2], // this is really required
#[as("t")]
time: f32, // will be referenced with "t" in the shader and is mandatory as well
#[unbound, as("bias")]
jitter: f32 // will be referenced with "bias" in the shader and is optional
}
}
That’s all for today! I hope that feature will be useful for whomever uses luminance – I use it
extensively and I’ve been using uniform_interface! for several days now and it’s really
appreciated! :D
I plan to add another macro to create buffer types, since they must meet GPU-specific alignment rules, who are boring to maintain without code generation.
Feel free to provide feedback on Reddit or GitHub and have fun!
]]>In a real time rendering system, it’s not uncommon finding constructs about assets. One famous construct is the resource manager. A resource manager is responsible of several tasks, among:
The first point is obvious, but the two others are less intuitive. (2) is important when the user might try to load the same object several times – for instance, a car model, or a character or a weapon. The most known strategy to prevent such a situation from happening is by using a software cache.
A software cache – let’s just say cache – is an opaque object that loads the object on the first request, then just returns that object for future same requests. For instance, consider the following requests and the corresponding cache behavior:
That behavior is very nice because it will spare a lot of computations and memory space.
(3) is about dependencies. For instance, when you load a car model, you might need to load its textures as well. Well, not really load. Consider the following:
You got the idea. (3) needs (2) to be efficient.
In imperative languages and especially in those that support template and/or generics, people tend to implement the cache system with an ugly design pattern – which is actually an anti design pattern : singleton. Each type of resource is assigned a manager by using a template parameter, and then if a manager needs to load a dependency, it just has to reference the corresponding manager by stating the type in the template parameter :
Model & getResource<Model>(std::string const &name) {
Texture &dependency = getResource<Texture>(...);
...
}
That way of doing might sound great, but eh, singletons are just global variables with a unicity constraint. We don’t want that.
We can use an explicit store object. That is, some kind of map. For instance, the store that holds textures would have a type like (in Haskell):
textureStore :: Map String Texture
A model store would have the following type:
modelStore :: Map String Model
And each stores is assigned a function; loadTexture, loadModel, and so on.
There are several drawbacks if we go that way. First, we have to carry all stores when using their functions. Some functions might need other stuff in order to resolve dependencies. Secondly, because of explicit state, we need to manually accumulate state! A loading function would have such a following type:
loadTexture :: Map String Texture -> String -> m (Texture,Map String Texture)
That will expose a lot of boilerplate to the user, and we don’t want that.
We can enhance the explicit store by putting it into some kind of context; for
instance, in MonadState. We can then write loadTexture to make it nicer to
use:
loadTexture :: (MonadState (Map String Texture) m,...)
=> String
-> m Texture
There is a problem with that. What happens when we add more types? For instance if
we want to handle textures and models? MonadState has a type family
constraint that forbids two instances for the pair s m. The following is not
allowed and will raise a compiler error:
instance MonadState (Map String Texture) MyState where
...
instance MonadState (Map String Model) MyState where
...
The solution to that problem is to have the carried state a polymorphic type and use typeclass constraint to extract and modify the map we want:
class HasMap a s where
extractMap :: s -> Map String a
modifyMap :: (Map String a -> Map String a) -> s -> s
With that, we can do something like this:
loadTexture :: (MonadState s m,HasMap Texture s,...)
=> String
-> m Texture
loadModel :: (MonadState s m,HasMap Texture s,HasMap Model s,...)
=> String
-> m Model
However, we hit a new issue here. What are s and m? Well, m doesn’t really
matter. For simplicity, let’s state we’re using a monad transformer; that is,
we use StateT s m as monad.
We still need s. The problem is that s has to be provided by the user. Worse,
they have to implement all instances we need so that the loading functions may
work. Less boilerplate than the explicit store solution, but still a lot of
boilerplate. Imagine you provide a type for s, like Cache. Expending the
cache to support new types – like user-defined ones – will be more extra
boilerplate to write.
The solution I use in my engine might not be the perfect solution. It’s not referentially transparent, an important concept in Haskell. However, Haskell is not designed to be used in convenient situations only. We’re hitting a problematic situation. We need to make a compromise between elegance and simplicity.
The solution required the use of closures. If you don’t know what a closure is, you should check out the wikipedia page for a first shot.
The idea is that our loading functions will perform some IO operations to
load objects. Why not putting the cache directly in that function? We’d have a
function with an opaque and invisible associated state. Consider the following:
type ResourceMap a = Map String a
getTextureManager :: (MonadIO m,...)
=> m (String -> m Texture)
getTextureManager = do
ref <- newIORef empty
pure $ \name -> do
-- we can use the ref ResourceMap to insert / lookup value in the map
-- thanks to the closure!
That solution is great because now, a manager is just a function. How would you
implement getModelManager? Well:
getModelManager :: (MonadIO m,...)
=> (String -> m Texture)
-> m (String -> m Model)
getModelManager loadTexture = ...
We can then get the loader functions with the following:
loadTexture <- getTextureManager
loadModel <- getModelManager loadTexture
And you just have to pass those functions around. The cool thing is that you can
wrap them in a type in your library and provide a function that initializes them
all at once – I do that in my engine. Later on, the user can extend the available
managers by providing new functions for new types. In my engine, I provide a few
functions like mkResourceManager that hides the ResourceMap, providing two
functions – one for lookup in the map, one for inserting into the map.
I truly believe that my solution is a good compromise between elegance and ease. It has a lot of advantages:
IORef with TVar or
similar objects for thread-safe implementations ;The huge drawback I see in that solution is its opacity. There’s also no way to query the state of each cache. Such a feature could be added by proving a new function, for instance. Same thing for deletion.
I’m one of those Haskellers who love purity. I try to keep my code the purest I can, but there are exceptions, and that cache problem fits that kind of exception.
Feel free to comment, and as always, keep the vibe and happy hacking!
]]>Well, pretty quickly! There’s – yet – no method to make actual renders, because I’m still working on how to implement some stuff (I’ll detail that below), but it’s going toward the right direction!
Something that is almost done is the framebuffer part. The main idea of framebuffers – in OpenGL – is supporting offscreen renders, so that we can render to several framebuffers and combine them in several fancy ways. Framebuffers are often bound textures, used to pass the rendered information around, especially to shaders, or to get the pixels through texture reads CPU-side.
The thing is… OpenGL’s framebuffers are tedious. You can have incomplete framebuffers if you don’t attach textures with the right format, or to the wrong attachment point. That’s why the framebuffer layer of luminance is there to solve that.
In luminance, a Framebuffer rw c d is a framebuffer with two formats. A
color format, c, and a depth format, d. If c = (), then no color will
be recorded. If d = (), then no depth will be recorded. That enables the use
of color-only or depth-only renders, which are often optimized by GPU. It
also includes a rw type variable, which has the same role as for Buffer.
That is, you can have read-only, write-only or read-write framebuffers.
And of course, all those features – having a write-only depth-only framebuffer for instance – are set through… types! And that’s what is so cool about how things are handled in luminance. You just tell it what you want, and it’ll create the required state and manage it for you GPU-side.
The format types are used to know which textures to create and how to attach
them internally. The textures are hidden from the interface so that you can’t
mess with them. I still need to find a way to provide some kind of access to the
information they hold, in order to use them in shaders for instance. I’d love to
provide some kind of monoidal properties between framebuffers – to mimick
gloss Monoid instance for its
Picture
type, basically.
You can create textures, of course, by using the createTexture w h mipmaps
function. w is the width, h the height of the texture. mipmaps is the
number of mipmaps you want for the texture.
You can then upload texels to the texture through several functions. The
basic form is uploadWhole tex autolvl texels. It takes a texture tex and
the texels to upload to the whole texture region. It’s your responsibility to
ensure that you pass the correct number of texels. The texels are represented
with a polymorphic type. You’re not bound to any kind of textures. You can pass
a list of texels, a Vector of texels, or whatever you want, as long as it’s
Foldable.
It’s also possible to fill the whole texture with a single value. In OpenGL
slang, such an operation is often called clearing – clearing a buffer,
clearing a texture, clearing the back buffer, and so on. You can do that
with fillWhole.
There’re two over functions to work with subparts of textures, but it’s not interesting for the purpose of that blog entry.
The cool thing is the fact I’ve unified pixel formats. Textures and
framebuffers share the same pixel format type (Format t c). Currently,
they’re all phantom types, but I might unify them further and use DataKinds to
promote them to the type-level. A format has two type variables, t and c.
t is the underlying type. Currently, it can be either Int32, Word32 or
Float. I might add support for Double as well later on.
c is the channel type. There’re basically five channel types:
CR r, a red channel ;CRG r g, red and green channels ;CRGB r g b, red, green and blue channels ;CRGBA r g b a, red, green, blue and alpha channels ;CDepth d, a depth channel (special case of CR; for depths only).The type variables r, g, b, a and d represent channel sizes.
There’re currently three kind of channel sizes:
C8, for 8-bit ;C16, for 16-bit ;C32, for 32-bit.Then, Format Float (CR C32) is a red channel, 32-bit float – the OpenGL
equivalent is R32F. Format Word32 (CRGB C8 C8 C16) is a RGB channel with
red and green 8-bit unsigned integer channels and the blue one is a 16-bit
unsigned integer channel.
Of course, if a pixel format doesn’t exist on the OpenGL part, you won’t be able to use it. Typeclasses are there to enforce the fact pixel format can be represented on the OpenGL side.
Currently, I’m working hard on how to represent vertex formats. That’s not a trivial task, because we can send vertices to OpenGL as interleaved – or not – arrays. I’m trying to design something elegant and safe, and I’ll keep you informed when I finally get something. I’ll need to find an interface for the actual render command, and I should be able to release something we can actually use!
By the way, some people already tried it (Git HEAD), and that’s amazing! I’ve
created the unstable branch so that I can push unstable things, and keep the
master branch as clean as possible.
Keep the vibe, and have fun hacking around!
]]>So here’s a quick article I can link in the super fresh TWiN, website I released a couple of days ago and that I hope will help with that kind of issues.
Hop has been around for some time now. I created it back in February 2021, so roughly one year and a half. It has changed and evolved quite a lot ever since. New hint algorithm, new commands, a very big number of new options and features. I really recommand people to read the embedded documentation of Hop. Yes, completely. It took me time to write in a way that it will take you a couple of minutes only to read. So yes, do read it. Get the habit to read the embedded documentation of the plugins you install, especially after new updates. Or read TWiN from now on 😁.
Yes, Hop has a wiki. It’s available here and you should read a couple of pages from it:
Because I know some people will not click on those links and will still open PRs trying to implement features that are already there, here is a non-exhaustive list of advanced things you can already do, out of the box, with Hop.
f, F, t and TIt’s in the wiki and super simple. For that, you just need three options:
direction, which must be set with require'hop'.HintDirection.BEFORE_CURSOR or
require'hop'.HintDirection.AFTER_CURSOR.current_line_only, a boolean.hint_offset, a super power allowing you to offset the actual jump off the jump target by a given number of
characters.So f is basically:
require'hop'.hint_char1({
direction = require'hop.hint'.HintDirection.AFTER_CURSOR,
current_line_only = true
})
F is then:
require'hop'.hint_char1({
direction = require'hop.hint'.HintDirection.BEFORE_CURSOR,
current_line_only = true
})
And in the same idea, t is:
require'hop'.hint_char1({
direction = require'hop.hint'.HintDirection.AFTER_CURSOR,
current_line_only = true,
hint_offset = -1
})
And T:
require'hop'.hint_char1({
direction = require'hop.hint'.HintDirection.BEFORE_CURSOR,
current_line_only = true,
hint_offset = -1
})
Hop already can already understand the concept of beginning and end of a jump target. It does not make sense with
all of them, but for most, it can. That is implemented with the hint_position option, which has to be one of:
require'hop.hint'.HintPosition.BEGINrequire'hop.hint'.HintPosition.MIDDLErequire'hop.hint'.HintPosition.ENDSo if you want to jump to the end of words:
require'hop'.words({
hint_position = require'hop.hint'.HintPosition.END,
})
You can even jump right after the end of a word!
require'hop'.words({
hint_position = require'hop.hint'.HintPosition.END,
hint_offset = 1
})
You can even use Hop to do something with the jump target that doesn’t imply jumping, like running a Lua function with
the actual column and line where the hint is. It’s done with require'hop'.hint_with_callback.
Here, no example but an exercise for you: try to use require'hop'.hint_with_callback to send a notification (via
vim.notify) displaying the jump target chosen by the user by hinting words. Tips: you will need to find the jump
target generator for words.
Please, feel free to read :h hop. It contains everything I just explained and so much more, like Hop extensions, multi
windows, etc. Have fun hopping around!
Currently, Kakoune doesn’t support writing to files with a timeout, which could help. kak-tree-sitter is a UNIX server bridging Kakoune with tree-sitter, supporting semantic highlighting, text-objects, indent guidelines and more!
I want to use the opportunity of releasing kak-tree-sitter in version v0.4.0 to talk about what’s new but also
explain a bit the new protocol used to make Kakoune communicate with the server and vice versa. I think it’s a pretty
fun (and efficient!) way of making things, and it can give people ideas.
This new version adds a bunch of things to kak-tree-sitter. Among the interesting things:
config.toml.C-c support to stop the server if started as standalone.tokio and replace with mio. It’s easier, faster and lighter.Additionnaly, you get v0.4.1, which also brings some interesting changes:
%opt{kts_lang} is now used to know how to highlight a buffer instead of %opt{filetype} directly. A hook
automatically calls kak-tree-sitter-set-lang (which you can override) to set %opt{kts_lang} before talking to the
server. By default, it simply forwards %opt{filetype}. The goal here is to allow people to provide their own
filetype detection by overriding kak-tree-sitter-set-lang.So yes, the big improvement is the introduction of a new communication protocol between Kakoune and kak-tree-sitter.
Before v0.4.0, the way things worked was:
kakrc, with a line such as
eval %sh{ kak-tree-sitter --kakoune --daemon --session $kak_session }.--daemon is passed, once the server is ready, it’s
going to daemonize so that its main process exits and Kakoune gets control back.--kakoune argument does one important thing: it asks the server to send some data back to the editor, once the
server is initialized. The data is sent back via the UNIX socket of Kakoune (kak -p). The session is known via
--session $kak_session.%sh{} block, and uses the
kak-tree-sitter -r interface to format requests as JSON.kak_command_fifo and kak_response_fifo) is used
to stream buffer content via write.What was the issue with that? Well, all those %sh{} implied to open a shell everytime we wanted to perform a request
to the server. And for highlighting, that happens very often (by default, the trigger hook runs for NormalIdle and
InsertIdle, but you can easily imagine that it runs almost on all keypresses modifying the window or the buffer).
That’s a lot. I quickly came to the realization that I wanted something… faster and with less layers.
See, this kak_command_fifo and kak_response_fifo are pretty interesting FIFO files. The idea is that Kakoune
handles their lifetimes, and they work when you do shell expansions (i.e. %sh{}). You can write to the
$kak_command_fifo Kakoune commands to be executed. This allows to execute Kakoune commands interleaved with shell
commands. You can then use $kak_response_fifo to write back results from Kakoune. I used to write the content of
buffers to $kak_response_fifo and pass the path to this FIFO in the highlight requests so that the server just had
to read from the FIFO and provide the highlights back to Kakoune asynchronously, via kak -p.
FIFOs are wonderful because you can think about them as a 1-1 blocking rendez-vous point between a producer and a
consumer. If a consumer arrives, it blocks until a writer arrives and starts writing, and a writer won’t write until a
consumer arrives. FIFOs do not live on your disk, so writing to them (i.e. write syscall) is super fast and can be
tought as memory-to-memory communication. It’s honestly pretty good.
However, we still have to open a shell to send the requests to the server with this approach… so I needed something else.
The idea is actually pretty simple: we can use FIFOs to send requests, in the end. If we have one FIFO for each Kakoune
session, we know that we should only have one writer (i.e. the Kakoune session), and we have a single consumer (the
KTS server). Inside Kakoune, writing to files can be done with the echo -to-file and write command, and we don’t
need a shell for that!
Now the important thing is that Kakoune doesn’t know the path to the FIFO to write requests to, so we still need a way to get that information. I had two different ideas:
$XDG_RUNTIME_DIR/kak-tree-sitter/commands/$kak_session.Currently, this is done with the --kakoune flag. The data that is sent back to the editor contains options
(e.g. %opt{kts_cmd_fifo_path}) so that we know where to write requests.
In the future, I might actually just standardize the location, since it’s not a random temporary directory.
The installed hooks, in the kak-tree-sitter group, will contain only a few options, mostly set in the global scope.
The main hook is run whenever a new window is open on a given buffer. A FIFO request is made to KTS with the filetype
(to be more correct, %opt{kts_lang}) and if KTS has the runtime files for this language (grammar, queries, etc.), it
will send back more data to Kakoune, that will be set for the buffer scope. This code will contain the actual
interesting hooks (e.g. NormalIdle and InsertIdle) that will make more FIFO requests to, for instance, highlightthe
buffer.
The content of the buffer is streamed via a FIFO, too — %opt{kts_buf_fifo_path}. Using this pair of FIFOs, we can
send requests to the server and stream buffer content by just using echo -to-file and write Kakoune commands. No
more shell expansions on the hot path!
Note: currently, buffers must be entirely read by kak-tree-sitter because the diffing algorithm is done by tree-sitter-highlight, which was great as a PoC, but not ideal now. Partial updates using
%val{history}and%val{uncommitted_modifications}are planned for a future release.
The only drawback of this approach is that, because we are writing to FIFO files… if one side of the channel is not there, the other side will block and hang. That could lead to Kakoune freezing (for instance, if you kill the server before trying to quit the editor). However, this is unlikely to happen and is an acceptable tradeoff to me.
Currently, Kakoune doesn’t support writing to files with a timeout, which could help.
This new release is exciting to me. I have learned many things, especially regarding mio vs. tokio (all of those
FIFOs requires a lot of IO synchronization) and optimizing. This new pair of FIFO per session made me realize that,
besides the initial “setup” that requires a shell, we can perfectly communicate with the external world by only writing
to files. In UNIX, everything is, indeed, a file!
The next releases will focus on two main topics:
If you would like to know more about kak-tree-sitter, head over
to the wiki, which is up to date with v0.4.1.
Have fun and keep the vibes!
]]>I have a MSI GS60 Ghost Pro 2QE I’m very proud of. It’s sexy and powerful. It comes with a fancy and configurable backlit keyboard.

There’s a tool called SteelSeries Engine for Windows we can use to change the colors of the keyboard. It supports several features:
Unfortunately, that software doesn’t work on Linux, even with wine. I tried hard to make it work and never actually found a way to run it. Then, I decided to look for alternatives and… found nothing working.
Yesterday, I tried a node.js-powered tool called msi-keyboard. And it worked. However, the interface and user interface was not my cup of tea. I decided to dig in in order to understand how it works, and I decided to write my own tool with a decent interface.
The key idea is that such keyboards are just HID devices. As a Haskell programmer, I looked for something that would grant me access to such devices, but nothing was working. There’s a hidapi package, but it doesn’t work and has bugs.
I didn’t give up though. I wrote my own Haskell HID API binding, called hid. Several good things about it:
Feel free to install and use it!
Then, I wrote another Haskell package, msi-kb-backlit. It might require super user rights to work. If you’re not a Haskeller, you can find installation details here.
Note: if you use Archlinux, you can directly download msi-kb-backlit through the AUR! Search for msi-kb-backlit with yaourt, or download the tarball.
The software has an embedded documentation to help you tweak with colors and modes. ;)
Feel free to use all those pieces of software. I made them with love for you all!
Enjoy your week end, and keep the vibe!
]]>The orphan instance problem is a well known problem that I’ve discovered while designing code in Haskell years ago. According to the Haskell wiki:
An orphan instance is a type class instance for class C and type T which is neither defined in the module where C is defined nor in the module where T is defined.
If it’s a bit overwhelming for you, here it is rephrased: this problem happens whenever you try to
create an instance of a typeclass C in a module that is not the module where C is defined for a
type T while you’re not in the module that actually introduces that T type. Since it’s not
possible to pick an instances by hand, GHC disables you from writing several instances for a same
combination of typeclass and types.
GHC actually has a workaround for the problem: the
{-# OPTIONS_GHC -fno-warn-orphans #-}pragma annotation (or the-fno-warn-orphanscompiler switch). However, this is not recommended if you can find another way because you expose yourself to ambiguities there.
Because Rust also has types and typeclasses (traits), it doesn’t escape the problem very much. You still have orphan instances in Rust (called Orphan Rules). There’re scoped to crates though, not modules.
In this blog entry, I want to explore a specific problem of orphans and how I decided to solve it in a crate of mine. The problem is the following:
Given a crate that has a given responsibility, how can someone add an implementation of a given trait without having to use a type wrapper or augment the crate’s scope?
The typical workaround to this problem is to use a type wrapper. It works by encapsulating the
origin type into another layer of typing so that the compiler recognize it as a complete different
type, allowing you to implement whichever traits you want. Rust has a nice feature added to that: if
you implement the Deref and DerefMut traits, you’re given access to all the implementations of
the source type via deref rules, which is pretty sick. This works if you’re writing a binary,
because no one will ever use your type wrapper, only you (and your team mates, but you’ll never
publish the type so you’re breaking the orphan problem). However, what happens if you’re writing
a library?
You will expose a type that is not the source one, forcing people to wrap it again if they want to implement another feature. If your type has a very different form from the source one, it’s okay. However, if you want to stick to the original semantics and use cases, people might get confused, especially whenever they will write functions that accept as arguments the source type.
splinessplines is a crate that provides you with spline interpolation. You can interpolate values in
several dimensions with several kind of interpolators (step, linear, cosine, etc.).
Clearly, the scope of splines is to provide math and curves types and functions, nothing more.
However, I use them in spectra, a demoscene crate of mine, in which I need them to implement
some serde traits in order to serialize and deserialize them. Are you starting to see the
problem?
When I thought about where I should write the code to allow serialization / deserialization, I came
across the realization that writing it directly in splines would add another set of dependencies
for everyone, even people not using the serialization part of it. I was a bit unsatisfied with
that. So I thought about “Why not just adding another crate, like splines-serde?” Obviously,
that doesn’t work because of the orphan rules: you cannot write an implementation of a trait in a
different crate than where the type or the trait is defined. Meh.
So I came with an idea, that is not perfect, but fits my needs pretty well. Rust has that
interesting concept of features, allowing for conditional compilation. cargo supports them in
the manifest and even allows you to declare dependencies as optional. The combination of both
features and optional dependencies is key here.
Let’s have a look at the current Cargo.toml manifest
in order to get how it’s done – I’ll slice the upper part and only show the features-related part:
[features]
default = ["std", "impl-cgmath"]
serialization = ["serde", "serde_derive"]
std = []
impl-cgmath = ["cgmath"]
[dependencies.cgmath]
version = "0.16"
optional = true
[dependencies.serde]
version = "1"
optional = true
[dependencies.serde_derive]
version = "1"
optional = true
Here, you can see that we are using default features. That means that if you depend on splines
in simple ways (for instance, by just giving a version string, i.e. splines = "0.2"), you’ll get
those features enabled by default. In the splines case, you’ll get the "std" and
"impl-cgmath" features by default.
Looking at the "std" feature, you can see that it doesn’t depend on anything else. That feature
will just make the whole library use the std crate. If you disable it, you can compile splines
with no_std.
The "impl-cgmath" has a dependency on cgmath and you can see that it depends on the "0.16"
version and that it’s optional. What it means is that if you disable default features you will not
depend on cgmath anymore and thus, you will not even download / compile the dependency.
If you look closely at that manifest, you also see a feature that must be set explicitly:
"serialization". That feature depends on both serde and serde_derive, adding support for the
serialization code we talked earlier.
All of that is great and cool but how do we write the impls based on that manifest?
There’re no rules here and people will give different advices on how you should write conditional
code. I tend to remain simple as long as the project is not too complex. splines is a pretty
simple and small project, so let’s get things straight!
The first thing to do is to know how we can access the features in Rust code. This is easy: via
attributes. Attributes in Rust are enclosed in square brackets preceded by a dash and bang –
#![]. There’s an interesting attribute: cfg(). It gives access to the configuration of the
project, for short. It may take several kind of parameters but we’re interested in only one:
feature. The syntax is the following:
#![cfg(feature = "name_of_feature")]
This oneliner will evaluate the following block whenever the "name_of_feature" is set. If you want
to evaluate a block if a feature is not set, you can use the not() combinator:
#![cfg(not(feature = "name_of_feature"))]
It’s a bit ugly, but it does the job.
One final and cool attribute we’ll be using: cfg_attr(). It takes two arguments: the first one is
a regular parameter you’d give to cfg that gets substituted as a boolean expression (feature(…),
not(feature(…)), etc.) and the second one is an attribute that will get set whenever the former
argument gets substituted successfully. For instance, this:
#![cfg(not(feature = "std"))]
#![no_std]
Can be rewritten more elegantly as this:
#![cfg_attr(not(feature = "std"), no_std)]
For the rest, as #[] applies to the direct item after it, it’s easy to write conditional
extern crate for instance:
// on no_std, we also need the alloc crate for Vec
#[cfg(not(feature = "std"))] extern crate alloc;
#[cfg(feature = "impl-cgmath")] extern crate cgmath;
#[cfg(feature = "serialization")] extern crate serde;
#[cfg(feature = "serialization")] #[macro_use] extern crate serde_derive;
The #[cfg_attr(…) is even nicer when wanting to insert attributes on a type definition, as with
the Key type:
#[derive(Copy, Clone, Debug)]
#[cfg_attr(feature = "serialization", derive(Deserialize, Serialize))]
#[cfg_attr(feature = "serialization", serde(rename_all = "snake_case"))]
pub struct Key<T> { … }
If you want static ifs in the actual implementation, you can cheat and use blocks as they’re items!
Interpolation::Cosine => {
let cp1 = &keys[i+1];
let nt = normalize_time(t, cp0, cp1);
let cos_nt = {
#[cfg(feature = "std")]
{
(1. - f32::cos(nt * consts::PI)) * 0.5
}
#[cfg(not(feature = "std"))]
{
use core::intrinsics::cosf32;
unsafe { (1. - cosf32(nt * consts::PI)) * 0.5 }
}
};
Some(Interpolate::lerp(cp0.value, cp1.value, cos_nt))
}
And finally, the one we were looking for to solve our orphans problem:
#[cfg(feature = "impl-cgmath")]
impl Interpolate for Vector2<f32> { … }
That impl will only exist if the "impl-cgmath" feature is set! Sweet!
I have another ultra cool use of attributes used along with existential impl Trait, but that’ll be
for another blog entry.
Keep the vibes!
]]>See, the type of application you need to write to make a demo is a bit special. Most programs out there are often either oneshot or reactive.
That binary split is not absolute, of course. Some programs don’t belong to any of the two prevous sections (think of a driver, a firmware, etc.). For the purpose of this article, though, it will be enough.
At some extent, we could state that a demoscene production, a video game or a simulation are also reactive programs. They mostly react to time as well as user interaction, network events, etc.. About time, you could imagine dividing time passing with very small quanta and picture time as a virtual clock that ticks such quanta. Every time a new quantum of time is emitted, the whole simulation is notified and reacts. However, representing that problem this way is not necessarily the best idea. See, a function of time is often a continuous function and then, it has an output value for any value of its input that lies in its domain. That property is interesting for us as we can get a value at any time with an arbitrary precision (the boundary being the precision at which we can represent a number on a computer).
The thing is, the kind of program we want generates its own inputs based on, mostly, the speed at which the hardware it’s running on is able to render a complete frame. The faster the more accurate we sample from that continuous function. That is actually quite logical: more FPS means, literally, more images to sample. The difference between two images will get less and less noticeable as the number of FPS rises. That gives you smooth images.
The “challenge” here is to write code to schedule those images. Instead of taking a parameter like the time on the command-line and rendering the corresponding image, we will generate a stream of images and will do different things at different times. Especially in demoscene productions, we want to synchronize what’s on the screen with what’s playing on the audio device.
From my point of view, we need at least two mechanisms of synchronization:
Both those problems are solved by two crates I wrote lately. Respectively, awoo and splines. This blog post is about awoo. splines already has its own dedicated articles here and here. Nevertheless, I will make another blog article about it because I have new ideas I will add to the crate to enrich the splines experience.
Taking on the example of the high-level synchronization described above, one can write quickly the following naive yet working snippet:
let mut time_ms: f32 = 0.;
loop {
if time_ms <= 5. {
// render scene 1
} else if time_ms <= 8. {
// render scene 2
} else if time_ms <= 20. {
// render scene 1 again
} else if time_ms <= 25. {
// render scene 3
} else
break; // quit
}
time_ms += 0.01; // 100 FPS
}
That code is typical in demoscene production when we have to rush or even if we have a few scenes to write. However, it has several problems:
ifs seem like naive and not necessary code.true, all the previous branches can be
completely discarded — we don’t have to test them anymore! — because time will never go
backwards in a simulation (that is a strong assumption and it’s not true if you’re debugging,
but for a release application, it is).So, how can we do better? The idea is actually pretty simple. We want a very simple form a
finite-state machine. In our case, the states are just what’s inside our ifs; the initial
state is the first scene being rendered and the transitions are a predicate on the current time.
Straightforward, right?
The idea of awoo is exactly that: allowing you to write the previous code like this:
let windows = vec![
Window::new(0., 5.).map(|_| println!("hey, it’s scene 1!")),
Window::new(5., 8.).map(|_| println!("hey, it’s scene 2!")),
Window::new(8., 20.).map(|_| println!("hey, it’s scene 1 again!")),
Window::new(20., 25.).map(|_| println!("hey, it’s scene 3!")),
];
let mut scheduler = SequentialScheduler::new(
SimpleF32TimeGenerator::new(0., 0.01),
windows
);
scheduler.schedule();
The code is now declarative and easier to read. Internally, the SequentialScheduler used here
will make a single test to know which code it has to run. The implementation is not the typical
implementation you would find for a FSM (finite-state machine), which uses a graph, but it’s akin.
You might be wondering why we do that map stuff instead of creating a Window directly with the
actions. The answer is simple: a Window doesn’t hold any actions. That allows for creating windows
via JSON, for instance, without having to deal with closures (I have no idea how that would even be
possible with JSON). The idea is then to zip your windows to your actions by using a hashmap,
for instance. This following snippet showcases exactly that
(fully available here):
const WINDOWS: &str = r#"
{
"a": {
"start": 0,
"end": 3
},
"b": {
"start": 3,
"end": 10
}
}"#;
let windows: HashMap<String, Window<f32>> = from_str(WINDOWS).expect("cannot deserialize windows");
let a = windows.get("a").unwrap().map(|t| println!("in a: {}", t));
let b = windows.get("b").unwrap().map(|t| println!("in b: {}", t));
}
What gets interesting is that you can write your own time generator to manipulate the simulation in other ways — and you can also use different schedulers regarding what you do with time. For instance, you can imagine implementing a time generator that gets time from a HTTP request, a keyboard, a network socket, etc. and then control your simulation with external stimuli.
What happens when the escape key is pressed and that you need to stop the simulation in order to quit? Simple: you need an interruptible scheduler. awoo offers that as well in this form:
use std::sync::mpsc::channel;
let (sx, rx) = channel();
let mut scheduler = create_your_scheduler();
scheduler.interruptible_with(move |_| {
// here, the closure’s argument is the time at which the scheduler is checking for interruptions
if let Ok(_) = rx.try_recv() {
Interrupt::Break
} else {
Interrupt::Continue
}
});
scheduler.schedule();
Here, we use the spinning loop of the scheduler to check for interruptions in a straight-forward way.
So far, I have to admit I haven’t digged the async, await and Future concepts in Rust too
much. For a single reason: discussions around those concepts have been heated and I will wait for an
official announcement of the feature. Schedulers, especially as simple as the ones in awoo, don’t
necessarily requires such IO features but the interruptible feature might. To me, the current
implementation of interruptible schedulers in awoo is sufficient, especially for animation
purposes — I might even add that feature directly in awoo so that you don’t have to do it by hand.
Currently, the crate’s scope is very narrow — and I actually like that. A tight and small scope implies a better visibility about what the crate must do and how it must do it. The crate is currently simple and it might get more and more complex stuff as needs appear. As I always tell other developers and engineers, I don’t like to overthink too much features I don’t even need. Obviously, it’s important to keep planning possible future additions… But not too much. This is why that crate’s scope, if augmented, will only and always revolve around the concept of scheduling animation code. It’s currently an experimental crate and I’m trying to write demos with it, so we’ll see what time thinks about it.
So that’s all for me for today. I hope you liked it. Keep the vibes!
]]>In Haskell, we have a lot of typeclasses. Those are very handy and – in general – come with laws. Laws are very important and give hints on how we are supposed to use a typeclass.
For instance, the Semigroup typeclass exposes an operator ((<>)) and has
an associativity law. If a, b and c have the same type T, if we know
that T is a Semigroup, we have the following law:
a <> b <> c = (a <> b) <> c = a <> (b <> c)
If T is a Monoid, which is a Semigroup with an identity (called mempty),
we have a law for monoids:
a <> mempty = mempty <> a = a
Those laws can – have – to be used in our code base to take advantage over the structures, optimize or avoid boilerplate.
In some situations, we want a way to express default values. That’s especially true in OO languages, like in the following C++ function signature:
void foo(float i = 1);
In Haskell, we cannot do that, because we have to pass all arguments to a function. The following doesn’t exist:
foo :: Float -> IO ()
foo (i = 1) = -- […]
And there’s more. Even in C++, how do you handle the case when you have several arguments and want only the first one to be defaulted? You cannot.
So, so… Some Haskellers decided to solve that problem with a typeclass. After all, we can define the following typeclass:
class Default a where
def :: a
We can then implement Default and have a default value for a given type.
instance Default Radians where
def = Radians $ 2*pi
instance Default Fullscreen where
def = Fullscreen False
However, there is an issue with that. You cannot use Default without creating
newtypes to overload them. Why? Well, consider the following Default
instance:
instance Default Float where
def = -- what should we put here?
Remember that, in Haskell, an instance is defined only once and is automatically imported when you import the module holding it. That means you cannot have the following:
-- in a module A
instance Default Float where
def = 0
-- in a module B
instance Default Float where
def = 1
-- in a module C
import A
import B
-- What instances should we use?
Hey, that’s easy. We just have to keep the modules apart, and import the one we want to use!
Yeah, well. No. No.

Orphan instances are wrong. You should read this for further explanations.
That’s why we have to use newtypes everywhere. And that’s boring. Writing code
should always have a goal. When we write as much boilerplate code as real code,
we can start thinking there’s something wrong. Worse, if we have more
boilerplate than real code, well, something is terribly wrong. In our case,
we’re introducing a lot of newtypes for only being able to use def at a few
spots. Is that even worth it? Of course not.
The Default typeclass is evil. It’s shipped with default instances, like one
for [a], which defaults to the empty list – []. It might be clear for you
but why would I want to default to the empty list? Why not to [0]? Or a more
complex list? I really doubt someone ever uses def :: [a].
Another reason why Default is wrong? There’s absolutely no law. You just
have a default value, and that’s all.
bar :: (Default a) => a -> Maybe String
Can you say what the default is for? Of course you cannot. Because there’s no law. The instance has no real meaning. A default value makes sense only for the computation using it. For instance, the empty list makes sense if we glue it to the list concatenation.
In base, there’re already several ways to express defaulted values.
The Monoid’s mempty is a way to express a default value regarding its
binary operation ((<>)).
The Alternative’s empty provides a similar default, but for first-class
types.
The MonadZero’s mzero provides a different default, used to absorb
everything. That’s a law of MonadZero:
mzero >>= f = mzero
a >> mzero = mzero
Those defaults are interesting because we can reason about. If I see mzero in
a monadic code, I know that whatever is following will be discarded.
So please. Stop using Default. It doesn’t bring any sense to your codebase. It
actually removes some! If you want to provide functions with defaulted
arguments, consider partially applied functions.

data-default is a very famous package – look at the downloads! You can now have some hindsight about it before downloading it and ruining your design. ;)
Happy hacking. :)
]]>Today is the end of May 2023. Helix has been my primary editor for months now. I haven’t come back to Neovim. In the previous article, I mentioned that I couldn’t give a fair opinion about Helix because I had just started using it. Today, I think I have enough experience and usage (5 / 6 months) to share what I think about Helix, and go a little bit further, especially regarding software development in general and, of course, editing software.
However, before starting, I think I need to make a clear disclaimer. If you use Vim, Neovim, Emacs, VS Code, Sublime Text, whatever, and you think that “Everyone should just use whatever they want”, then we agree and this is not the point of this blog article. The goal is to discuss a little bit more than just speaking out obvious takes, but please do not start waving off the topic because you think “Everyone should use what they want”. There is a place for constructive debate even there. If you start arguing, then it means you want to debate, and then you need to be ready to have someone with different arguments that will not necessarily go your way, nor your favorite editor.
Finally, if you think I haven’t used Vim / Neovim enough, just keep in mind that I have been using (notice the tense) Vim (and by extension, Neovim) since I was 15, and I’m 31, so 16 years.
I have wrote that blog article already three times before deleting everything and starting over. I think I will make another blog article about how I think about software, but here I want to stay focused on one topic: editors and development environments.
From a vimmer perspective, Helix is weird. It reverses the verb and the motion (you don’t type di( to delete inside
parenthesis, but you type mi(d to first match the parenthesis and then delete). Then, you have the multi-selections
as a default way of doing things; Helix doesn’t really have the concept of a cursor, which is a design it gets from
Kakoune, and I’ll explain what it means and implies later.
Some vimmers publicly talked about it. ThePrimeagen, for instance, made a Youtube video about it where he basically just scratched the surface of the editor and “Hell it’s not Vim so it’s not really good”. He moved the video to private then but I’m sure you can just look for Tweets. Many Vim afficionados react that way, which is not a very serious way of trying out something new, especially if you happen to publicly talk about it to thousands of people. I think it’s not fair to both the tool (here, Helix) and the people reading you, unless you are one of those Vim zealots thinking you know everything better than everyone else and dismissing people’ points of views just because they are not the same as yours.
Anyway, that reputation didn’t hold me from trying out, and the way I try software is simple: just give in and accept to drop your habits and productivity. Of course, at work, I was still using Vim / Neovim, but switched to Helix on my spare-time projects.
There are many aspects to talk about with Helix. The first one is that, contrary to what people who haven’t really and seriously tried it, the difference with Vim is not only “reversed motion/verb and multi-cursors.” The first difference is the design and the direction.
Helix is an editor that natively integrates many features that are most of the time plugins in others editors (even
in VS Code). The best examples here are tree-sitter, surrounding pairs (
being able to automatically surround regions of text with (, [, HTML tags, etc.), LSP UIs, fuzzy pickers, etc. This
is a massive and important difference because of two main things:
About code maintenance, having all of those features natively integrated in the editor means that you are 100% sure that if you get the editor to start, the features will work, and the editor developers will keep that updated within the next iteration of the editor. For instance, having tree-sitter natively implemented (Rust) in Helix means that the editor itself knows about tree-sitter and its grammars, highlights queries and features. The same thing goes for surrounding-pairs or auto-pairs, for instance. If the team decides to change the way text is handled in buffers, then the code for auto-pairs / surrounding-pairs will have to be updated for the editor to be releasable.
The user experience will then be better, because you get those nice features without having to start looking around for a plugin doing it, with the problem of chosing the right one among a set of competing plugins. Plus the risk of having your plugin break because it’s not written by the Helix team. Just install the software and start using those features.
For now, Helix ships with a bunch of powerful features that makes it usable in a modern environment:
hx --grammar.catppuccin themes are there!).There is also one (major) thing I want to talk about and that deserves its own section: configuration.
Helix treats configuration the way it should be: as data. Configuration in Helix is done in TOML. There is no scripting implied, it’s only a rich object laid out as TOML sections. And this is a joy to use. For instance, this is my current Helix configuration (excluding keys remapping, because my keyboard layout is bépo):
theme = "catppuccin_macchiato"
[editor]
scroll-lines = 1
cursorline = true
auto-save = false
completion-trigger-len = 1
true-color = true
color-modes = true
auto-pairs = true
rulers = [120]
idle-timeout = 50
[editor.cursor-shape]
insert = "bar"
normal = "block"
select = "underline"
[editor.indent-guides]
render = true
character = "▏"
[editor.lsp]
display-messages = true
display-inlay-hints = true
[editor.statusline]
left = ["mode", "spinner", "file-name", "file-type", "total-line-numbers", "file-encoding"]
center = []
right = ["selections", "primary-selection-length", "position", "position-percentage", "spacer", "diagnostics", "workspace-diagnostics", "version-control"]
Notice the tree-sitter and LSP configuration. Yes. None.
This is so important to me. Because configuration is data, it is simple for Helix to expose it and present it to the
user by reading the TOML file without caring about having any side-effects. Helix has a command line (:) where you can
tweak those options dynamically. And you can dynamically reload the configuration as well.
The major difference, even before the reversed motion/verb thing, is the fact that Helix doesn’t really have a cursor. It has the concept of selections. A selection is made of two entities:
The cursor is the part of the selection that moves when you extend the selection. The anchor, as the name implies, is
the other part that stays where it is: it’s anchored. By default, you have only one selection and the anchor is
located at the same place as the cursor. It looks similar to any other editor. Things start to change when you begin
typing normal commands. Typing l, for instance, will move both the anchor and cursor to the right, making them a
single visual entity. However, if you type w, the cursor will move to the end of the word while the anchor will move
to its beginning, visually selecting the word. If you type W, the anchor won’t move and only the cursor will move,
extending the selection. If you press B, it will move the cursor back one word, shrinking the selection. You can press
<a-;> to flip the anchor and the selection, which is useful when you want to extend on the left or on the right.
This concept of selection is really powerful because everything else is based on it. Pressing J will move the cursor
down one line, leaving the anchor on the current line, extending selected lines. Once something is selected, you can
operate on it, with d, c, y, r, etc. For instance, wd will select the word and delete it. Jc will extend the
selection with the next line and start changing. Selections in Helix are not just visual helps: they represent what
normal editing operations will work on, which is a great design, because you can extend, shrink and reason about them in
a much more seamless and natural way.
But it’s just starting. Remember earlier when I say that by default, you have only one selection? Well, you can have
many, and this is where Helix starts to really shine to me. The first way to create many selections is to press C. C
will duplicate your current selection on the next line. If you have the anchor at the same position as the cursor,
pressing C will make it like you have another cursor on the next line below. For instance, consider this text:
I love su|shi.
But I also love pizza.
The | is our cursor (but also anchor). If you press C, you will see something like this:
I love su|shi.
But I als|o love pizza.
But as I had mentioned, C duplicates selections. Let’s say you started like this, < being the anchor and | the
cursor:
I love <sus|hi.
But I also love pizza.
Pressing C now will do this:
I love <sus|hi.
But I a<lso| love pizza.
Once you have many selections, everything you type as normal commands will be applied to every selections. If I type
f., it will set the anchor to the current cursor and extend the cursor to the next ., resulting in this:
I love sus<hi|.
But I also< love pizza|.
Press <a-;> to swap anchors and cursors:
I love sus|hi<.
But I also| love pizza<.
And then pressing B will do this:
I love |sushi<.
But I |also love pizza<.
Erratum:
Bis not defined to this behavior in a vanilla Helix. I have remapped it to the Kakoune behavior. It doesn’t change much to what I’m saying here, though.
Multi-cursor is then not only a nice visual help, but also a completely new way of editing your buffers. Once you get the hang of it, you don’t really think in terms of a single cursor but many selections, ranges, however you like to call them.
C is great, but this not something we usually use. Instead, we use features that don’t exist in Vim. I’m not entirely
sure how to call those, but I like to call them selection generators. They come in many different flavors, so I’ll
start with the easiest one and will finish with the most interesting and (maybe a bit?) obscure at first.
The m key is Helix is a wonderful key. It’s the match key. It expects a motion and will change all your selections
to match the motion. For instance, mia “matches inside arguments” (tree-sitter). Imagine this context:
fn foo(x: i32, y: |i32) {}
fn bar(a: String, |b: bool) {}
Press mia to get this:
fn foo(x: i32, <y: i32|) {}
fn bar(a: String, <b: bool|) {}
Then, for instance, press <a-;> to flip the anchor and the cursor:
fn foo(x: i32, |y: i32<) {}
fn bar(a: String, |b: bool<) {}
F, to select the previous ,:
fn foo(x: i32|, y: i32<) {}
fn bar(a: String|, b: bool<) {}
Erratum: same thing as with
B; I have remapped it in my config. The defaultBdoesn’t extend like this.
And just press d:
fn foo(x: i32) {}
fn bar(a: String) {}
It’s so logical, easy to think about and natural. Someting interesting to notice, too, is that contrary to Vim, which
has many keys doing mainly the same thing, making things weird and not really well designed. For instance, vd selects
the current character and delete it. vc deletes the current character and puts you into insert mode. s does exactly
the same. Why would you have a key in direct access doing something so specific? If you want to delete a word, you
either press vwd, or more simply in Vim, dw. All of that is already confusing, but it doesn’t end there. x deletes
the current character, but x is actually cut, so if you select a line with V and press x, it will cut the line.
Press Vc to change a line… or just S. What?
All those shortcuts feel like exceptions you have to learn, and it’s a good example of a flawed design. On the other
side, Helix (which is actually a Kakoune design it’s based on), have a single character to delete something: d. Since
the editor has multi-selections as a native editing entity, all of those situations will imply using the d key:
d.wd.xd (x selects and extend the line).md(.That applies to everything.
The design of editing in Helix (Kakoune) is to be interactive and iterative. For instance, consider the following:
pub fn new(
line_start: usize,
col_start: usize,
line_end: usize,
col_end: usize,
face: impl Into<String>,
) -> Self {
Self {
line_start,
col_start,
line_end,|
col_end,
face: face.into(),
}
}
Let’s say we would like, to begin with, select very quickly every arguments type and switch them to i32. Many ways
of doing that, but let’s see one introducing a great concept: selecting. Selecting allows you to create new
selections that satisfy a regex. The default keybinding for that is s for… select (woah). s always applies to the
current Selections (notice the use of plural, it will be useful later). We then need to start selecting something. Here,
we can just press mip to select inside the paragraph, since our cursor is right after line_end:
< pub fn new(
line_start: usize,
col_start: usize,
line_end: usize,
col_end: usize,
face: impl Into<String>,
) -> Self {
Self {
line_start,
col_start,
line_end,
col_end,
face: face.into(),
}
}|
We have the whole thing selected. Press s to start selecting with a regex. We want the arguments, so let’s select
everything with : and press s: and return:
pub fn new(
line_start<:| usize,
col_start<:| usize,
line_end<:| usize,
col_end<:| usize,
face<:| impl Into<String>,
) -> Self {
Self {
line_start,
col_start,
line_end,
col_end,
face<:| face.into(),
}
}
See how it created a bunch of selections for us. Also, notice that it selected face: face.into(), which is not
correct. We want to remove that selection. Again, several ways of doing it. Something to know is that, Helix (Kakoune)
has the concept of primary selection. This is basically the selection on which you are going to apply actions first,
like LSP hover, etc (it would be a madness to have LSP hover applies to all selections otherwise!). You can cycle the
primary selection with ( and ). Once you reach the one you want, you can press <a-,> to just drop the selection.
However, we don’t want to cycle things. We want a faster way.
Let’s talk about removing selections. The default keybinding is <A-K>. However, here, our selections are all
about the same content (the :). As mentioned before, pressing x will select the current line of every selections:
pub fn new(
< line_start: usize,|
< col_start: usize,|
< line_end: usize,|
< col_end: usize,|
< face: impl Into<String>,|
) -> Self {
Self {
line_start,
col_start,
line_end,
col_end,
< face: face.into(),|
}
}
Let’s filter selection and remove the one matching a pattern. In our case, let’s remove selections with a
( in them: <A-K>\( and return:
pub fn new(
< line_start: usize,|
< col_start: usize,|
< line_end: usize,|
< col_end: usize,|
< face: impl Into<String>,|
) -> Self {
Self {
line_start,
col_start,
line_end,
col_end,
face: face.into(),
}
}
Now press _ to shrink the selections to trim leading and trailing whitespaces:
pub fn new(
<line_start: usize,|
<col_start: usize,|
<line_end: usize,|
<col_end: usize,|
<face: impl Into<String>,|
) -> Self {
Self {
line_start,
col_start,
line_end,
col_end,
face: face.into(),
}
}
Imagine that we have changed our mind and now we actually want to change the usize to i32. We can use the keep
operator, which is bound to K by default. Press Kusize and return to get this:
pub fn new(
<line_start: usize,|
<col_start: usize,|
<line_end: usize,|
<col_end: usize,|
face: impl Into<String>,
) -> Self {
Self {
line_start,
col_start,
line_end,
col_end,
face: face.into(),
}
}
Another possible way is to press susize to only select the usize directly, which might be wanted if you want to
change them quickly to i32, for instance.
The last operation that I want to mention is splitting. It’s introduced with S and will spawn several cursors
separated by a regex. For instance, consider:
let |array = [1.0, 20.32, 3., 4.35];
Let’s say you’d like to select the numbers. With the methods described above, it’s probably challenging. With the
splitting command, it’s much easier. Put your cursor anywhere in the list and press mi[ to select inside of it:
let array = [<1.0, 20.32, 3., 4.35|];
Then simply press S, to split the selection into selections separated by commas. You should end up with this:
let array = [|1.0>,| 20.32>,| 3.>,| 4.35>];
Pressing _ will shrink the selections to remove leading and trailing spaces:
let array = [|1.0>, |20.32>, |3.>, |4.35>];
And here you have it! Now remember that you can combine all of this methods with semantic objects, like mif for
inside functions, mat for around type, mia for inside arguments, and many more. Recall that you can do that
on each selection, allowing for really powerful workflows.
Hell yes. It’s a muscular memory thing. For instance, I oftentimes the need to not only replace occurrences of
patterns, like fooN — with N being a number — into logger.fooN, but I often need to change the structure around
those occurrences. And here, Helix really stands out. In Vim, you’d have to use a super ugly regex, completely blind,
and eventually a macro. The interactive and iterative approach of Helix is so much more powerful to me. For instance,
for the case described above: % to select the whole buffer, sfoo. to select foo with a single character
afterwards, then return, clogger.foo to replace with logger.foo, and still in insert mode, <C-r>" to paste what
was yanked by the c operation. Here, the default register, ", makes a lot of sense, because this register is local
to each selection, making this replace operation trivial and interactive.
Another example is something like this:
const COLORS: [Color; 3] = [
Color {
r: 255,
g: 0,
b: 0,
},
Color {
r: 0,
g: 255,
b: 0,
},
Color {
r: 0,
g: 0,
b: 255,
},
];
Imagine that you want to change every Color constructor to a function call, that does something like
Color::rgb(r, g, b). Doing that interactively and iteratively in Helix is so easy. I’d put my cursor anywhere in that
block, press mi[ to select everything inside [], then sColor<cr> to create three cursors on the Cursor, and from
that moment, it’s just ping-pong-ing between normal mode and insert mode like you would do with a single selection. You
will be using f, t, miw etc. select things but the idea is the same, and the three occurrences will be updated at
once.
Contrary to other famous editors and IDEs, Helix is not supposed to be extendable; it doesn’t try to solve more problems than it should (and you will see in the Kakoune section that we can even push that to another extreme). Something like Neovim is a bit of a disguised IDE. Yes, a vanilla Neovim with no plugins and no configuration is just a very basic (and I would dare say featureless) editor. It won’t have LSP working. It won’t have tree-sitter working either. Nothing for git integrated. Nothing for delimiters, nothing for pickers, nothing for any modern development. The power of something like Emacs, Neovim etc. is to build on extensibility.
I used to enjoy that, until I came to the realization that, perhaps, it would be great to put things into perspective: is extensibility something we actually want? What do we try to solve with it? Well, we extend tools to add new features and new behaviors. We extend things so that the native / core tool ships with minimal features but doesn’t prevent people from adding specific and customized capabilities.
But is extensibility the only way to achieve that goal? The thing with extensibility is that:
.so / .dylib / .dll), a scripting language, a JIT, etc.The last point is important. Extending a software requires the environment to adapt to the specifities of what you’re extending, and that will require the environment to know about the specifities. Here, the environment could be anything outside of Neovim. What it means is that, you’re not going to use external tools, but you are going to use the interfaces, scripting languages, DSLs etc. of the tool you want to extend.
For instance, people might argue that extending Neovim is great because it only requires learning Lua, which is not specific to Neovim, but that it is not actually true nor acurate. You have to learn “Neovim Lua”, which is basically its own beast. It’s like the standard library, but for Neovim. It will provide you with APIs you can use — and only use — to add new features to Neovim.
The same argument can be made to any extensible editor. VS Code, Emacs, etc.
Another way to add features and behaviors is to add them externally, by leveraging the tools themselves and compose them instead of pushing more features into them. That vision is not very popular and famous and I’m not entirely sure why. For instance, Vim has vim-fugitive, a Git client for Vim / Neovim. It has 2k commits, between 8k-9k lines of code… and can be used only in Vim and Neovim. Yes, it extends and adds features inside those editors, but still. If at some point you decide to switch to another editor, you can forget about this plugin. This is even worse with something like magit, which is the best Git client I haver ever used. Yet I don’t use it anymore, because it’s an Emacs plugin. What a shame.
Now repeat that reasoning for all the plugins you use. That has led some people to just install as few plugins as possible and switch to composability.
Composability is the same concept as in code. You have two systems A and B doing thingA and thingB, and you
write some piping glue code / script to connect both. People use many different languages to glue things together.
Among the most famous approaches:
fish).I have shell functions that I source when I start my shell; for instance, one called p, to switch to my spare-time
projects:
p () {
proj_dir=${PROJ_DIR:-~/dev}
project=$(ls $proj_dir | sk --prompt "Switch to project: ")
[ -n "$project" ] && cd $proj_dir/$project
}
This is a great example of composability, which composes the content of a project directory (the $PROJ_DIR environment
variable, or ~/dev by default), with the sk fuzzy finder, and then changes the directory to whatever the user has
picked. I use that script all the time to quickly move to my various projects.
Notice that, sk, fzf, etc. already are tools that implement fuzzy searching for arbitrary inputs. Tools such as
find or fd, who are so far in my experience the fastest programs to look for stuff, can be composed with shell
pipes as well and integrated into your work environment.
Then the question starts to appear: why do editors / plugins re-implement all of that to have it inside the editor? It’s pretty apparent in the Neovim community, but it often ends up with abandonware plugins, and harder to maintain editors.
While I was wondering about all that, I was getting pretty productive with Helix, enjoying its simpler design, data configuration, better editing features and overall way more stable experience. I remembered that most of the Helix design came from Kakoune. And I started to think about one (not so) crazy idea: should I have a look at Kakoune?
And let’s enter Kakoune.
As mentioned above, Helix is heavily inspired by Kakoune. The main difference, from the surface, with distance, is that Helix comes with more bundled features, like LSP, tree-sitter, pickers, etc. However, there are more (drastic) differences that I need to talk about.
The first thing is, again, the design. And for that, I really need to quote a part from the excellent design doc written by @mawww. The part that is the most interesting to me is, obviously, composability.
Being limited in scope to code editing should not isolate Kakoune from its environment. On the contrary, Kakoune is expected to run on a Unix-like system alongside a lot of text-based tools, and should make it easy to interact with these tools.
For example, sorting lines should be done using the Unix sort command, not with an internal implementation. Kakoune should make it easy to do that, hence the
|command for piping selected text through a filter. The modern Unix environment is not limited to text filters. Most people use a graphical interface nowadays, and Kakoune should be able to take advantage of that without hindering text mode support. For example, Kakoune enables multiple windows by supporting many clients on the same editing session, not by reimplementing tiling and tabbing. Those responsibilities are left to the system window manager.
And this is one of the most important things about Kakoune to me. First, it goes into the direction I’ve been wanting for years (I’ll let you read my previous blog articles about editors and production environments; I’m basically saying the same thing). And second, it ensures that the native editor remains small and true to its scope, ensuring an easier maintenance, hence less bugs and more stable. Also, it doesn’t blindly ignore what everyone else is doing.
Let’s start with an example. Yes, Kakoune doesn’t have a fuzzy picker to pick your files. However, as mentioned above, it composes well with its environment. It does that via different mechanisms (shell blocks, FIFOs, UNIX sockets, etc.). Here, we can just use whatever we like to get a list of files, and let Kakoune ask the user which files to open. We then simply use the selected value and open it. In order to do that, you need to read the design doc to understand a couple of other things, such as the section about interactive use and scripting. Quoting:
As an effect of both Orthogonality and Simplicity, normal mode is not a layer of keys bound to a text editing language layer. Normal mode is the text editing language.
Typing normal commands in a .kak file is then the way to go. And then, coming back to the fuzzy picker example, here
are three commands defined in my kakrc:
## Some pickers
define-command -hidden open_buffer_picker %{
prompt buffer: -menu -buffer-completion %{
buffer %val{text}
}
}
define-command -hidden open_file_picker %{
prompt file: -menu -shell-script-candidates 'fd --type=file' %{
edit -existing %val{text}
}
}
define-command -hidden open_rg_picker %{
prompt search: %{
prompt refine: -menu -shell-script-candidates "rg -in '%val{text}'" %{
eval "edit -existing %sh{(cut -d ' ' -f 1 | tr ':' ' ' ) <<< $kak_text}"
}
}
}
As you can see, it’s just about composing known and well written tools together. Another example? Alright. Kakoune doesn’t have splits, but I still want them. Let’s go:
## kitty integration
define-command -hidden kitty-split -params 1 -docstring 'split the current window according to the param (vsplit / hsplit)' %sh{
kitty @ launch --no-response --location $1 kak -c $kak_session
}
## zellij integration
define-command -hidden zellij-split -params 1 -docstring 'split (down / right)' %sh{
zellij action new-pane -cd $1 -- kak -c $kak_session
}
define-command -hidden zellij-move-pane -params 1 -docstring 'move to pane' %sh{
zellij action move-focus $1
}
## tmux integration
define-command tmux-split -params 1 -docstring 'split (down / right)' %sh{
tmux split-window $1 kak -c $kak_session
}
define-command tmux-select-pane -params 1 -docstring 'select pane' %sh{
tmux select-pane $1
}
The design is not extensible: it’s composable, and all in all, it makes so much more sense to me.
If you are used to Helix, then Kakoune with a bit of configuration will feel very similar to Helix. Of course, you will have to look around for LSP and tree-sitter support. The way we do that is by adding external processes to interact with Kakoune servers / clients via UNIX sockets, FIFO pipes, etc.. Kakoune doesn’t know anything about LSP or tree-sitter, but you can write a binary in any language you want and send remote commands to control the behavior of Kakoune.
The interesting aspect with those tools is that, in theory, we could adapt them to make them editor independent. If more editors adopted the strategy of Kakoune (composing via the shell), we wouldn’t even have to write other binaries to add LSP / tree-sitter support, which is an interesting aspect.
I plan on writing a blog article detailing the design of kak-tree-sitter, because I think it’s a good source of
knowledge regarding UNIX and tree-sitter.
Besides that, Kakoune is way more mature than Helix, in the sense that it has some specificities to some edge cases with
multi-selection features (such as, what happens when you have multiple cursors inside text looking like function
argument lists, and you type mia to select them, but some selections are actually not arguments? Kakoune will remove
the mismatched selections, which is what we would expect, while Helix…… erm it’s complicated! but currently, it will
keep the selections around, which is confusing and dangerous).
Kakoune has a wonderful feature called marks. Marks are different from what you have in Vim. They use a specific register to record the current selections and eventually restore them later, supporting merging selections and editing commands. An example that I love doing; imagine the following snippet:
Thank you to [@NAME](https://github.com/NAME) for their contribution.
Let’s say you want to have a cursor on each NAME. Easy, with s. You just select the whole thing (you can select the
whole line with x for instance), then sNAME, return, then c to start changing with whatever you want.
Now, imagine this instead:
Thank you to [@](https://github.com/) for their contribution.
How do you insert at the same time at the right of @ and before the )? Getting two selections will be hard (or you
will end up writing crazy regexes; remember, we are not using Vim anymore!). A super easy way to do it is to move your
selection to @:
Thank you to [@|](https://github.com/) for their contribution.
Press Z to register the current selection (you have only one). Then move the cursor to the ):
Thank you to [@](https://github.com/|) for their contribution.
And then press <a-z>a to merge the current selection to the one(s) already stored. You end up with this:
Thank you to [@|](https://github.com/|) for their contribution.
Two cursors at the right places! This is extremely powerful and a feature that should arrive in Helix, but not sure exactly when.
A pretty other important thing to say about Kakoune is that it has a server/client design that allows to share session contexts. That is the main mechanisms used to implement native splits (via Kitty, tmux, whatever), but also many other features, such as project isolation, viewing the same buffers with different highlighters, etc. etc.
There is one important thing I need to mention. I have been playing with Kakoune for a while now, and I have been
working on kak-tree-sitter for almost as long as I’ve been working Kakoune (couple of months). And there is one issue
with the UNIX approach.
See, tree-sitter, LSP, DAP, git gutters, etc. All those things are pretty fundamental to a modern text editor. Externalizing them (Kakoune) is an interesting take, but I’m not entirely sure Kakoune is still doing it completely correctly.
The main problem is social intelligence. Because all tooling is now externalized, many people can come up with
their own efforts. For instance, kak-lsp and kak-tree-sitter are completely separate projects, and they should
remain that way (for many reasons; scopes, maintenance, dependencies, etc.). However, in order to operate on the editor
contents, both programs must interact with the editor. That implies:
This problem is important, because dumping the whole content of a big buffer to an external process is one thing, doing
it for every different external processes is a massive overhead. Because I have read a bit the source code of kak-lsp,
I know that we (kak-tree-sitter) are doing similar things: we do use FIFOs to write buffer content and communicate
with our servers / daemons without going through the disk. But we are doing it that twice. And it’s just two projects;
any dissociate projects needing to access the buffer content will probably perform similar things.
That is a massive problem to me and I’m not sure how I feel about it. I’m not against sending the content of a buffer via a FIFO to an externalized program — I actually think it’s a pretty good design —, but doing it for every integration… I’m not exactly sure what would be the best solution, but maybe something that would snapshot a buffer inside some POSIX shared memory (with mutable lock access if needed) could be one way to go. Honestely, I am not sure.
All of that to say that, the take of Helix is pretty good here, because all of those UNIX problems are not there in that
editor: everything runs in the same process, inside the same memory region. I will come back to this problem with my
next article on kak-tree-sitter and its design.
Today, I’m mainly using both Kakoune and Helix. Helix still has some bugs, even with tree-sitter (which my daemon doesn’t have, funnily!), so I sometimes use one or the other tool.
I have learned so many things lately, with both Helix and Kakoune (especially Kakoune, it made me love UNIX even more). All of that echoes the disclaimer I made earlier: yes, Vim and Neovim are good, but Kakoune and Helix are so much better to me. Better designs, better editing experiences, largely snappier, more confidence in the direction of the native code (because a much, much smaller codebase). Helix is written in Rust, Kakoune in C++, but what matters is the actual design. To me, Kakoune is by far the best designed software I have ever seen, and for that, I really admire the work of @mawww and everyone else involved in the project. My contribution to the Kakoune world with kak-tree-sitter is, I hope, something that will help and drive more people in. I will write another blog article about that, with the pros., cons. and tradeoffs. of composability in editors.
In the meantime, have fun and use the editor you love, but remember to have a look around and stay open to change! Keep the vibes!
]]>I would like to thank @Taupiqueur, who played an important role into making me undersand Kakoune and eventually fall in love with — the editor, I mean! :)
Among all the new features of luminance-0.40, there is one that I think is worth explaining in details, because it’s… pretty cool. If you like type states, refinement typing and on a general level, strong typing, you should enjoy this blog article.
While working on luminance-0.40, I spent two weeks working on a feature that is going to have a huge beneficial impact on how people use luminance. I’ve been wanting to see what people think about it for a long time, because I think it’s both a very powerful feature and allows to do low-level memory stuff in a safe way via the type system. Without further ado, let’s dig in.
In pre luminance-0.40, it is possible to gather vertices inside a type called Tess (it
stands for tessellation). That type is responsible for holding vertices to render them later.
They might represent point clouds, triangle-based meshes, lines, etc. It contains several properties,
but we are going to focus on three:
In luminance-0.39, vertices, indices and instances can be retrieved, both read-only on
read-write, via a mechanism of GPU slicing: you ask luminance to give you a type with which you are
able to use &[T] and &mut [T].
However… we have several problems. First, the type is Tess and is completely monomorphized. What
it means is that if you create a Tess that contains vertices of type SimpleVertex with indices
of type u8, that information is not encoded in the type system — it is actually encoded as a
value that is read and checked at runtime! When you ask for a slice, for instance to mutate the
content of the Tess, you have to use a function like Tess::as_slice<V> where V: Vertex, which
expects you to pass the type of stored vertices — in our case, it would be SimpleVertex. What
happens if someone passed the wrong type? Well, currently, luminance checks at runtime that the
stored type is the right one, but this is both wasted checks and not a very elegant API.
The same applies to indices and instance data: you don’t see them in the type. What happens if
you slice the indices with u32? Runtime checks.
Now, there’s also the problem of building. When you build a Tess, you have to pass the vertices
to the TessBuilder type, using functions like TessBuilder::add_vertices. It works, but
it’s not practical. More importantly: if you call several times that function, you will create
something that is called a deinterleaved memory data structure. Let’s digress a bit about that.
Interleaving and deinterleaving makes sense when we talk about several objects / items of a same
type, laid out in memory — typically in arrays. Imagine a type, Vertex, such as:
struct Vertex {
position: Point3D,
color: RGBA,
}
If you take a vector of Vertex (Vec<Vertex>), you get a continuous array of properties in
memory. If you represent that in memory, you have something similar to this for two vertices
(padding omitted):
[x0, y0, z0, r0, g0, b0, a0, x1, y1, z1, r1, g1, b1, a1]
We say that the memory is interleaved, because you’re going to alternate between fields when
iterating in memory. Everything is interleaved. This kind of memory is what happens when you put a
struct in an array, slice, etc. and is perfectly suited for most situations (even on the GPU).
However, there is a small (yet important) detail: when you iterate (for instance in a for loop)
on that array, you’re going to ask your CPU / GPU to load a bunch of values at once. That is going
to fill your cache lines. If at each iteration you need to use every fields of the vertex, then
that situation is pretty convenient, because you’re going to have a bunch of fields cached ahead
(cache hits).
However… imagine a loop that only needs to access the position field. What’s going to happen is
that your CPU / GPU will still load the same data in cache lines: now you get colors in the cache
that you don’t need and your loop will make more cache misses. What could have been better would
have been to fill the cache lines only with positions. If we had, instead:
[x0, y0, z0, x1, y1, z1]
[r0, g0, b0, a0, r1, g1, b1, a1]
Those two vectors can then be used independently for each need. Because we only need positions, we can simply use the first position vector. Now, when the CPU / GPU is going to load something in the cache, it’s going to cache much more values that we are going to actually use: we get more cache hits and it’s playa party.
That kind of memory layout is called deinterleaved memory. The way we typically do that is by
simply moving the fields out of the struct and make several arrays of each field.
People tend to use two terms to describe both layouts: AoS and SoA.
So… months years ago, I realized that and decided a needed I better plan. Especially, on
luminance-0.39, the way you handle slicing deinterleaved data is… well, inexistent. You
cannot slice such data because it was never supported.
The new type is the following:
pub struct Tess<B, V, I = (), W = (), S = Interleaved>
where ABunchOfThings;
As you can see, there are a lot of new things there:
B: the new backend type variable that most types now have; used to forward backend
implementations down the API.V: the vertex type. It’s the type of Vertex you have typically defined yourself using
luminance-derive.I: the index type. Most of the time, you’ll use either u8, u16 or u32, depending on
the kind of geometry you index.W: the vertex instance data. Because people might not want that, () is a good default, but
if you really need it, you can basically use any Vertex type here, since vertex instance data
must be part of a Semantics.S: the interleaving mode. Either Interleaved or Deinterleaved.You will find the same type variables with the TessBuilder type.
The cool thing about that change is how it enabled me to yield much, much better APIs. Consider
the previous API to create a deinterleaved Tess:
let direct_deinterleaved_triangles = TessBuilder::new(&mut surface)
.add_vertices(TRI_DEINT_POS_VERTICES)
.add_vertices(TRI_DEINT_COLOR_VERTICES)
.set_mode(Mode::Triangle)
.build()
.unwrap();
Notice the two add_vertices. There is no type information checking and ensuring that:
Vertex type those fields correspond
to.add_vertices as many times as you want and get a big buffer in GPU
memory that will make no sense.Now, the new API looks like this:
let direct_deinterleaved_triangles = surface
.new_deinterleaved_tess::<Vertex, ()>()
.set_attributes(&TRI_DEINT_POS_VERTICES[..])
.set_attributes(&TRI_DEINT_COLOR_VERTICES[..])
.set_mode(Mode::Triangle)
.build()
.unwrap();
If you try to call set_vertices — the name got changed from add_vertices to set_vertices
— on the builder you get from new_deinterleaved_tess, you will get a compilation error, because you
cannot set vertices on deinterleaved tessellations: you need to set attribute vectors. The
set_attributes has the information that you are doing that for a Vertex, so it can check the
input data you pass and ensure it contains values which type is a field type used in Vertex. If
not, you get a compilation error.
Most importantly: because of how vertices work in luminance, a field type is unique to a vertex: it
doesn’t make sense to use twice the VertexPosition type. If you end up in such a situation, it
means that your Semantics type lacks another variant — remember: vertex fields are basically
semantics-based attributes. That leads to the possibility to automatically find out where exactly
the data you provide needs to go inside the GPU tessellation.
The super cool part is that you can now slice deinterleaved tessellations by simply asking for:
In our case, we have a deinterleaved tessellation, which means we cannot slice whole vertices. If
you try to get a slice of Vertex, you will get a compilation error. However, we can retrieve slices
of vertex fields. The way we do this is super simple: we simply call the tess.vertices() or
tess.vertices_mut() methods. It will infer the type of slices you are asking to automatically
slice the right GPU buffer. This is all possible because our types are unique as the vertex fields.
let positions = tess.vertices().unwrap(); // you have to check for errors normally
let colors = tess.vertices().unwrap(); // you have to check for errors normally
Remark: you need to have the types of
positionsandcolorsinferred by setting / reading / passing them around, or you will have to put type ascriptions so thatvertices()know what fields you are referring to.
So let’s dig a bit into how all this works. The first thing you need to know is that deinterleaving — the raw concept — is really simple. If you have a type such as:
struct Foo {
a: A,
b: B,
c: C,
}
We say that Vec<Foo> is the interleaved representation of the collection. The deinterleaved
representation needs to have three vectors of fields:
vec_a: Vec<A>
vec_b: Vec<B>
vec_c: Vec<C>
In order for a Vertex type to be valid in deinterleaving contexts, we need to have that
tuple of vectors representation. First, we need a mapping between Vec<Foo> to
(Vec<A>, Vec<B>, Vec<C>). This is the role of two traits: Deinterleave<T> and
TessVertexData<S>.
Deinterleave<T> gives, for T, the field rank for T. For instance:
<Foo as Deinterleave<A>>::RANK == 0<Foo as Deinterleave<B>>::RANK == 1<Foo as Deinterleave<C>>::RANK == 2This is mandatory so that we know exactly which GPU buffers will need to be read / written to
when creating tessellations and slicing them. You don’t have to implement Deinterleave<T> by
yourself: luminance-derive does that automatically for you when you create a Vertex type.
Also, you might be tempted to think that this rank will be used inside the Vertex type to
retrieve data, but since you cannot pass whole vertices… nah. Also, you shouldn’t assume ranks
based on fields declarations in struct (rustc can re-order that).
Next is TessVertexData<S>. It associates an input type — the type of data a tessellation will
receive at creation type — for S, for the implementor type. The easy one
is TessVertexData<Interleaved> for V where V: Vertex. The associated type is simply Vec<V>,
because interleaved geometry simply stores the vertices as a vector of the whole vertex type.
Simple.
It gets more complicated when we talk about deinterleaved geometry.
TessVertexData<Deinterleaved> for V where V: Vertex has its associated type set to…
Vec<DeinterleavedData>. Indeed: there is no simple way with the current stable Rust (and even
nightly) to know the full type of Vertex fields at compile-time here. However, don’t get it wrong:
that Vec is not a vector of vertices. It’s a typically small set of attributes (vectors too). If
you look at the definition of DeinterleavedData, you get this:
#[derive(Debug, Clone)]
pub struct DeinterleavedData {
raw: Vec<u8>,
len: usize,
}
Yep. Type erasure at its finest. When you pass deinterleaved data, the data is type-erased and passed as a big blob of bytes, glued with its original size (so that we don’t have to store typing information – this will be needed when slicing vertex attributes).
Implementing slicing with these traits and data types is now possible: we can add another trait
that we will use to slice vertices, for instance (the backend::VertexSlice trait in luminance)
and based on the type of T in Deinterleave<T>, we can go and grab the GPU buffer we want.
For instance, in both the OpenGL and WebGL luminance backends, buffers are stored in a Vec<_>,
so in order to know which one we need to lookup, we simple use the
<Vertex as Deinterleave<T>>::RANK constant value (a usize) and we’re good to go. You
need two other traits for vertex instance data (InstanceSlice) and vertex indices (IndexSlice),
and you’re good to go.
So, checking at compile-time deinterleaving, in luminance, is done by:
Deinterleave<T>) to associate a rank to a field T in a data structure.TessVertexData<S>) to get the input type of a tessellation: either Vec<T> for
the whole vertices, or Vec<DeinterleavedData> for a set of attributes).vertices_mut::<T>(), we basically simply require
Deinterleave<T> for V so that we can get a RANK: usize. We can then, in the backend
implementation, find the right GPU buffer and slice it. The reconstruction to a typed slice
(&[T]) is statically assured by the length stored in the DeinterleavedData type and by the
fact DeinterleavedData<T> is implemented for V.A small note though. luminance — actually, I do that with all my code, whatever the language —
considers a lot of operations unsafe. In this case, implementing Deinterleave<T> is unsafe.
The reason is pretty obvious: you can implement Deinterleave<Whatever>, even if your vertex
type doesn’t have a Whatever field. Doing so would yield a hazardous / undefined behavior when
slicing the GPU buffer. luminance-derive takes care of implementing the unsafe interface for
you.
I hope you have learned something new or gotten some ideas. Keep the vibes!
]]>On the last weekend, I was at the Revision demoparty (Easterparty). It’s a meeting of friends and great people doing graphics, music, code and that kind of fun and pretty stuff. I didn’t release anything, but I’ve been working on a release for a while now, and I was lacking something in my engine: light shafts.
I know a few ways to do that. They almost all fall in one of the two following classes:
Both the techniques produce interesting images. However, the screen-space-based method produces bad results when you don’t look directly to the light – it may even produce nothing if the light is out of screen – and the raymarching-based is a step process that might generate artifacts and can be slow.
The idea I had is very simple. I haven’t tested it yet, but it’s planned for very soon. I’ll post images with benchmarks as soon as I have something on screen. I’m not sure it’s an unknown way to do it. I just haven’t found anything describing that yet. If you have, please leave a link in the comments below! :)
The idea is the following. You need a depthmap. If you’re used to shadow mapping you already have a depthmap for your light. In order to simplify this, we’ll use the depthmap used for shadow mapping, but I guess it’s totally okay to use another depthmap – we’ll see that it could be even handier.
For each point in that depthmap is the distance – in a specific space coordinates system – of the corresponding point in world space to the position of the light. If you have the projection and the view matrices of the light, it’s easy to deproject the depthmap. What would we get if we deproject all the depthmap texels into the world space? We’d get the exact lit surfaces.
For a spot light, you can imagine the deprojected version of the depthmap as a cone of light. The “disk” of the cone will deform and shape as the lit environment. That’s the first part of the algorithm.
We have a points cloud. What happens if, for each deprojected texel – i.e. point – we draw a line to the position of the light? We get an actual lines field representing… photons paths! How amazing is that?! Furthermore, because of the depthmap sampling, if you look at the light and that an object is between you and the light, the photons paths won’t go through the obstructing object! Like the following image:

Of course, because of using raw lines, the render might be a bit blocky at first. If you know the laser trick1 – i.e. quad-based lasers, you can apply it to our lines as well, in order to get better results. The first improvement is to disable depth test and enable additive blending.
In the first place, you need to generate the depthmap of the light. Then, you need to extract the volumetric shaft. You’ll need a vertex buffer for that. Allocate w*h elements, where w and h are the depthmap’s width and height. Yeah, a point per texel.
Create a shader and make a render with glDrawArrays(GL_POINTS, 0, w*h) with an attributeless
vertex array object. Don’t forget to bind the depthmap for use in the shader.
In your vertex shader, use gl_VertexID to sample from the depthmap. Transform the resulting
texel in world-space. You can use something like the following deprojection formula:
vec3 deproject() {
float depth = 2. * texelFetch(depthmap, ivec2(gl_FragCoord.xy), 0).r - 1.;
vec4 position = vec4(vv, depth, 1.);
position = iProjView * position;
position.xyz /= position.w;
return position.xyz;
}
Pass that to the next stage, the geometry shader. There, build whatever kind of new primitive you want. In the first place, I’ll go for a simple line connected to the light’s position. In further implementation, I’ll go for lasers-like base shapes, like star-crossed quads.
In the fragment shader, put whatever color you want the shaft to have. You could use interpolation to reduce lighting wherever you want to create nice effects.
Don’t forget to use additive blending, as we do for lasers.
I see two major problems. The first one is the bright aspect the shaft will have if you don’t blend correctly. Play with alpha to reduce more if the fragment is near the light’s position and make the alpha bigger when you’re far away from the light’s position. Because you’ll blend way more photons paths near the light’s position than far from it.
The second issue is the resolution of the extracted volume. For a 512x512 depthmap, you’ll get around 262k points, then 524k lines. That’s a lot for such an object. And that’s only for a simple spot light. An omnidirectional light would require six times more lines. What happens if we don’t use lines, but star-crossed quads an that we want several shafts? You see the problem.
A solution could be to sample from high mipmap level, so that you don’t use the full resolution of the shadow map. That would result in less visual appealing shafts, but I’m pretty sure it’d be still good. You could also branch a blur shader to smooth out the whole thing.
I’ll try to implement that as soon as possible, because I think my idea is pretty interesting compared to raymarching, which is expensive, and way better than screen-space, because the shaft will still be visible if the light goes out of screen.
I’ll write an article about it if you don’t – leave a comment for asking
I’ve made a listing with pros and cons. in the README file of luminance. I’ll just copy that section and report it below so that you’re not even a click away from the listing!
A lot of folks have asked me on IRC / Reddit / gitter / etc. how luminance compares to other famous frameworks. Here are a few comparisons:
glium has been around for a while – way longer than luminance.
According to crates.io, glium is no longer actively developed by its original author.
impl, which is neat – however, this tends to be less and less
true as luminance has been getting new macros lately.glium::VertexBuffer
type – a buffer to hold vertices – while luminance gives you a Tess type that hides that kind
of complexity away for you.Frame object in glium and passing the whole pipeline at once.
The pipeline then must be completely resolved prior to render. See
this. luminance is more
flexible on that side since it enables you to provide the rendering pipeline in a dynamic way,
allowing for tricky structure traversal without cloning nor borrowing, for instance. glium
requires a tuple of (vertices, indices, program, uniforms, render_state) while luminance works
by building the AST node by node, level by level.uniform!
macro when invoking the draw command on your Frame. This has the effect to lookup the uniform
in the shader for every call – modulo caching – and requires you to explicitly pass the uniforms
to use. luminance uses a contravariant approach here: you give the type of the uniform
interface you want to use and you will be, later, handed back a reference to an object of that
type so that you can alter it. All of this is done at the type level; you have nothing to do when
passing values to the shader in the pipeline. Opposite approaches, then. Also, notice that
luminance looks up the uniform names exactly once – at Program creation – and provides several
hints about uniform utilization (inactive, type mismatch, etc.). glium seems to have runtime
type checking as well, though.[[f32; 4] 4]
in both crates).Frame of glium above.gfx is a high-performance, bindless graphics API for the Rust programming language. It aims to be the default API for Rust graphics: for one-off applications, or higher level libraries or engines.
The current listing concerns gfx in its’ pre- low level situation. Recent gfx APIs are different.
gfx_defines! macro
to easily introduce the types and make named bindings (to map to vertex attributes in shaders). A
lot of boilerplate is generated thanks to that macro, which is neat. luminance has a similar
scheme even though the macro support in luminance is less advanced. On a general note:
luminance is less macro-driven.gfx_defines! macro. They’re declared in very
similar ways as in luminance. However, gfx uses the same syntax as with vertex definition,
while luminance has an ad hoc approach – it’s typical Rust code with annotations. That doesn’t
make a huge difference per-se. gfx calls uniforms constant, which is nice because it can
abstract over the concept, while luminance doesn’t and uses the terms Uniform, Uniformable,
UniformInterface, etc.gfx_defines! macro invokations. This is
seriously cool and luminance suffers a bit from not having that – defining pipeline in
luminance is a bit boring because of lambda / closures boilerplate.create_vertex_buffer_with_slice, for instance. This is nice as it enables coping with
multi-threading safety, which is something luminance doesn’t support yet (it’s planned, though).gfx_app::Application, defined in the gfx_app crate.On a general note, luminance and gfx pre-ll are way too different to be compared to each other. If you want to target several backends, do not hesitate and go to gfx. If you target only one system and want simplicity, go to luminance.
ggez is a Rust library to create a Good Game Easily.
As ggez is primarily focused on 2D projects, it doesn’t compare directly to luminance, as it’s about graphics, sound, input, etc. Also, ggez is using gfx.
vulkano is a wrapper around the Vulkan graphics API.
The scope is not really the same as vulkano only targets Vulkan and – as for today – luminance is about OpenGL.
amethyst is a fast, data-oriented, and data-driven game engine suitable for rapid prototyping and iteration.
As stated in the current document, luminance is not a game engine. So if you’re looking for a tool that already has everything needed to build a game, amethyst should be your toy. Also, it’s using gfx.
piston is a modular game engine written in Rust.
As for other game engines, nothing to compare here since luminance is not a game engine!
spectra is a demoscene framework / engine mainly written for demoscene purposes, even though it can now be used to some other extents (animation / video game).
]]>spectra uses luminance as primary graphics backend, but it provides way more features than just graphics. You’ll find audio, animation, editing, resource system, custom shading language, etc.
I’ll be talking about:
I’ve tried a lot of editors in the last ten years. I spent a year using emacs but eventually discovered – erm, learned! – vim and completely fell in love with the modal editor. It was something like a bit less than ten years ago. I then tried a lot of other editors (because of curiosity) but failed to find something that would help be better than vim to write code. I don’t want to start an editor war; here’s just my very personal point of view on editing. The concept of modes in vim enables me to use a very few keystrokes to perform what I want to do (moving, commands, etc.) and I feel very productive that way.
A year ago, a friend advised me to switch to neovim, which I have. My editor of the time is then neovim, but it’s so close to vim that I tend to use (neo)vim. :)
I don’t use any other editing tool. I even use neovim for taking notes while in a meeting or when I need to format something in Markdown. I just use it everywhere.
I use several plugins:
Plugin 'VundleVim/Vundle.vim'
Plugin 'ctrlpvim/ctrlp.vim'
Plugin 'gruvbox'
Plugin 'neovimhaskell/haskell-vim'
Plugin 'itchyny/lightline.vim'
Plugin 'rust-lang/rust.vim'
Plugin 'jacoborus/tender.vim'
Plugin 'airblade/vim-gitgutter'
Plugin 'tikhomirov/vim-glsl'
Plugin 'plasticboy/vim-markdown'
Plugin 'cespare/vim-toml'
Plugin 'mzlogin/vim-markdown-toc'
Plugin 'ElmCast/elm-vim'
Plugin 'raichoo/purescript-vim'
Plugin 'easymotion'
Plugin 'scrooloose/nerdtree'
Plugin 'ryanoasis/vim-devicons'
Plugin 'tiagofumo/vim-nerdtree-syntax-highlight'
Plugin 'mhinz/vim-startify'
Plugin 'Xuyuanp/nerdtree-git-plugin'
Plugin 'tpope/vim-fugitive'
Plugin 'MattesGroeger/vim-bookmarks'
Plugin 'luochen1990/rainbow'
A package manager. It just takes the list you read above and clones / keeps updated the git repositories of the given vim packages. It’s a must have. There’re alternatives like Pathogen but Vundle is very simple to setup and you don’t have to care about the file system: it cares for you.
This is one is a must-have. It gives you a fuzzy search buffer that traverse files, MRU, tags, bookmarks, etc.

I mapped the file search to the , f keys combination and the tag fuzzy search to , t.
The colorscheme I use. I don’t put an image here since you can find several examples online.
This colorscheme also exists for a lot of other applications, among terminals and window managers.
Because I write a lot of Haskell, I need that plugin for language hilighting and linters… mostly.
The Lightline vim statusline. A popular alternative is Powerline for instance. I like Lightline because it’s lightweight and has everything I want.
Same as for Haskell: I wrote a lot of Rust code so I need the language support in vim.
A colorscheme for statusline.
I highly recommend this one. It gives you diff as icons in the symbols list (behind the line numbers).

GLSL support in vim.
Markdown support in vim.
TOML support in vim.
TOML is used in Cargo.toml, the configuration file for Rust projects.
You’re gonna love this one. It enables you to insert a table of contents wherever you want in a Markdown document. As soon as you save the buffer, the plugin will automatically refresh the TOC for you, keeping it up-to-date. A must have if you want table of contents in your specifications or RFC documents.
Elm support in vim.
Purescript support in vim.
A must have. As soon as you hit the corresponding keys, it will replace all words in your visible buffer by a set of letters (typically one or two), enabling you to just press the associated characters to jump to that word. This is the vim motion on steroids.
A file browser that appears at the left part of the current buffer you’re in. I use it to discover files in a file tree I don’t know – I use it very often at work when I work on a legacy project, for instance.
A neat package that adds icons to vim and several plugins (nerdtree, ctrlp, etc.). It’s not a must-have but I find it gorgeous so… just install it! :)
Add more formats and files support to nerdtree.
A cute plugin that will turn the start page of vim into a MRU page, enabling you to jump to the given file by just pressing its associated number.
It also features a cow that gives you fortune catchphrases. Me gusta.
Git support in nerdtree, it adds markers in front of file that have changes, diff, that were added, etc.
A good Git integration package to vim. I use it mostly for its Gdiff diff tooling directly in vim,
even though I like using the command line directly for that. The best feature of that plugin is
the integrated blaming function, giving you the author of each line directly in a read-only vim
buffer.
Marks on steroids.
This little plugins is very neats as it allows me to add colors to matching symbols so that I can easily see where I am.

I’m a rustacean. I do a lot of Rust on my spare time. My typical workflow is the following:
cargo commands:
cargo watch -x test running; this
command is a file watcher that will run all the tests suites in the project as soon as a file
changes;cargo watch – similar to cargo watch -x check
running; this command is a file watcher that will proceed through the compilation of the binary
but it will not compile it down to an actual binary; it’s just a check, it doesn’t produce
anything you can run. You can manually run that command with cargo check. See more
here;cargo check by hand and run cargo build to test the
actualy binaryC-z and the jobs and
fg commands. I do that especially to create directories, issue git commands, deploy things,
etc. It’s especially ultra cool to run a rg search or even a find / fd. I sometimes do that
directly in a neovim buffer – with the :r! command – when I know I need to edit things from the
results. Bonus: refactoring with tr, sed and find -exec is ultra neat.The workflow is almost exactly the same besides the fact I use the stack build --fast --file-watch
command to have a file watcher.
Haskell’s stack doesn’t currently the awesome
checkcommand Rust’s cargo has. Duh :(
I also have a similar workflow for other languages I work in, like Elm, even though I use standard unix tools for the file watching process.
Aaaah… git. What could I do without it? Pretty much nothing. git is such an important piece of
software and brings such an important project philosophy and behaviors to adopt.
What I find very cool in git – besides the tool itself – is everything around it. For instance, on
GitHub, you have the concept of Pull Request (PR) – Merge Request in GitLab (MR). Associated with a
good set of options, like disallowing people to push on the master branch, hooks, forcing people
to address any comments on their MR, this allows for better code reviews and overall a way better
quality assurance of the produced code than you could have without using all this infrastructure.
Add a good set of DevOps for deployment and production relatide issues and you have a working team
that has no excuse to produce bad code!
My neovim fugitive plugin allows me to open a special buffer with the :Gblame command that gives
me a git blame annotation of the file. This might be trivial but it’s very useful, especially at
work when I have my colleagues next me – it’s always better to directly ask them about something
than guessing.

Another one that I love is :Gdiff, that gives you a git diff of the modification you’re about to
stage. I often directly to a git diff in my terminal, but I also like how this plugin nails it as
well. Very pratictal!

It’s always funny to actually witness difference in workflows, especially at work. People who use
mostly IDEs are completely overwhelmed by my workflow. I was completely astonished at work that some
people hadn’t even heard of sed before – they even made a Google search! I’m a supporter of the
philosophy that one should use the tool they feel comfortable with and that there’s no “ultimate”
tool for everyone. However, for my very own person, I really can’t stand IDEs, with all the buttons
and required clicks you have to perform all over the screen. I really think it’s a waste of time,
while using a modal editor like neovim with a bépo keyboard layout (French dvorak) and going back
and forth to the terminal is just incredibly simple, yet powerful.
I had a pretty good experience with Atom, a modern editor. But when I’ve been told it’s written with web technologies, the fact it’s slow as fck as soon as you start having your own tooling (extensions), its pretty bad behavior and incomprensible “Hey, I do whatever the fck I want and I’ll just reset your precious keybindings!” or all the weird bugs – some of my extensions won’t just work if I have an empty pane open, wtf?!… well, I was completely bolstered that GUI interfaces, at least for coding and being productive, are cleary not for me. With my current setup, my hands never move from the keyboard – my thrists are completely static. With all the candies like easymotion, ctrlp, etc. etc. I just can’t find any other setups faster and comfier than this one.
There’s even an extra bonus to my setup: because I use mostly unix tools and neovim, it’s pretty straigth-forward to remote-edit something via ssh, because everything happens in a terminal. That’s not something you can do easily with Atom, Sublime Text or any other editors / IDEs – and you even pay for that shit! No offence!
However, there’s a caveat: because pretty much everything I do is linked to my terminal, the user experience mostly relies on the performance of the terminal. Using a bad terminal will result in an overall pretty bad experience, should it be editing, compiling, git or ssh. That’s why I keep lurking at new terminal emulaters – alacritty seems very promising, yet it’s still too buggy and lacks too many features to be production-ready to me – but it’s written in Rust and is GPU-accelerated, hells yeah!
Whoever you are, whatever you do, whomever you work with, I think the most important thing about workflow is to find the one that fits your needs the most. I have a profound, deep digust for proprietary and closed software like Sublime Text and IDEs that use GUIs while keyboard shortcut are just better. To me, the problem is about the learning curve and actually wanting to pass it – because yes, learning (neo)vim in details and mastering all its nits is not something you’ll do in two weeks; it might take months or years, but it’s worth it. However, as I said, if you just feel good with your IDE, I will not try to convert you to a modal editor or a unix-based workflow… because you wouldn’t be as productive as you already are.
Keep the vibe!
]]>Yeah, Rust. It’s a strong and static language aiming at system programming. Although it’s an imperative language, it has interesting functional conventions that caught my attention. Because I’m a haskeller and because Rust takes a lot from Haskell, learning it was a piece of cake, even though there are a few concepts I needed a few days to wrap my mind around. Having a strong C++11/14 experience, it wasn’t that hard though.
The first thing that amazed me is the fact that it’s actually not that different from Haskell!
Rust has a powerful type system – not as good as Haskell’s but still – and uses immutability as
a default semantic for bindings, which is great. For instance, the following is forbidden in Rust
and would make rustc – the Rust compiler – freak out:
let a = "foo";
a = "bar"; // wrong; forbidden
Haskell works like that as well. However, you can introduce mutation with the mut keyword:
let mut a = "foo";
a = "bar"; // ok
Mutation should be used only when needed. In Haskell, we have the ST monad, used to introduce
local mutation, or more drastically the IO monad. Under the wood, those two monads are actually
almost the same type – with different warranties though.
Rust is strict by default while Haskell is lazy. That means that Rust doesn’t know the concept of memory suspensions, or thunks – even though you can create them by hand if you want to. Thus, some algorithms are easier to implement in Haskell thanks to laziness, but some others will destroy your memory if you’re not careful enough – that’s a very common problem in Haskell due to thunks piling up in your stack / heap / whatever as you do extensive lazy computations. While it’s possible to remove those thunks by optimizing a Haskell program – profiling, strictness, etc., Rust doesn’t have that problem because it gives you full access to the memory. And that’s a good thing if you need it. Rust exposes a lot of primitives to work with memory. In contrast with Haskell, it doesn’t have a garbage collector, so you have to handle memory on your own. Well, not really. Rust has several very interesting concepts to handle memory in a very nice way. For instance, objects’ memory is held by scopes – which have lifetimes. RAII is a very well known use of that concept and is important in Rust. You can glue code to your type that will be ran when an instance of that type dies, so that you can clean up memory and scarce resources.
Rust has the concept of lifetimes, used to give names to scopes and specify how long an object reference should live. This is very powerful yet a bit complex to understand in the first place.
I won’t go into comparing the two languages because it would require several articles and a lot of spare time I don’t really have. I’ll stick to what I’d like to tell you: the Rust implementation of luminance.
The first very interesting aspect of that port is the fact that it originated from a realization while refactoring some of my luminance Haskell code. Although it’s functional, stateless and type-safe, a typical use of luminance doesn’t really require laziness nor a garbage collector. And I don’t like using a tool – read language – like a bazooka. Haskell is the most powerful language ever in terms of abstraction and expressivity over speed ratio, but all of that power comes with an overhead. Even though you’ll find folks around stating that Haskell is pretty okay to code a video game, I think it will never compete with languages that are made to solve real time computations or reactive programming. And don’t get me wrong: I’m sure you can write a decent video game in Haskell – I qualify myself as a Haskeller and I’ve not been writing luminance just for the joy of writing it. However, the way I use Haskell with luminance shouldn’t require all the overhead – and profiling got me right, almost no GC was involved.
So… I looked into Rust and discovered and learned the language in only three days. I think it’s due to the fact that Rust, which is simpler than Haskell in terms of type system features and has almost everything taken from Haskell, is, to me, an imperative Haskell. It’s like having a Haskell minus a few abstraction tools – HKT (but they’ll come soon), GADTs, fundeps, kinds, constraints, etc. – plus a total control of what’s happening. And I like that. A lot. A fucking lot.
Porting luminance to Rust wasn’t hard as a Haskell codebase might map almost directly to Rust. I had to change a few things – for instance, Rust doesn’t have the concept of existential quantification as-is, which is used intensively in the Haskell version of luminance. But most Haskell modules map directly to their respective Rust modules. I changed the architecture of the files to have something clearer. I was working on loose coupling in Haskell for luminance. So I decided to directly introduce loose coupling into the Rust version. And it works like a charm.
So there are, currently, two packages available: luminance, which is the core API, exporting the
whole general interface, and luminance-gl, an OpenGL 3.3 backend – though it will contain more
backends as the development goes on. The idea is that you need both the dependencies to have access
to luminance’s features.
I won’t say much today because I’m working on a demoscene production using luminance. I want it to be a proof that the framework is usable, works and acts as a first true example. Of course, the code will be open-source.
The documentation is not complete yet but I put some effort documenting almost everything. You’ll find both the packages here:
I’ll write another article on how to use luminance as soon as possible!
Keep the vibe!
]]>rustc. Especially, I haven’t seen
a mention of that property in the book, so I’m happily sharing.
The weird situation is about super traits. A super trait is a trait that must be implemented
for another trait to be usable, because it’s relied on. Traits can be thought of as
constraints, so a super trait is a bit like a dependency when implementing a trait, and an
implication when using a trait on which a super trait is declared. A trait can have zero to
several super traits (added with the + operator). For instance, imagine you have a trait Alive:
trait Alive {
fn get_health(&self) -> Health;
}
Now imagine you want a trait to move someone or something around:
trait Move: Alive {
fn go(&mut self, direction: Direction);
}
Here, you can see that:
Move requires to implement Alive, because it’s a super trait. It’s a dependency.Move requires Alive, Alive is implied when you use Move. Indeed, that would be
redundant to annotate a type T: Move + Alive, because an instance (implementor) for Move
cannot exist without Alive to be implemented as well.So now that we understand what super traits are, let’s get to the weird stuff.
When you implement a trait which has a super trait, do you think that:
The distinction is important. (1.) doesn’t require rustc to prove when implementing a trait
that the super trait is implemented. That will be required when using the trait. With (2.), rustc
will have to prove that the super trait is, first, implemented, before even considering the
implementation of the trait you’re making.
Rust currently uses (2.). If you impl Move for Foo, impl Alive for Foo must be in scope for
that implementor to be possible.
But… it would be interesting to actually have (1.). Imagine that you want to implement Move for
something a bit complex, like Creature<T>, but not all Creature<T> are alive. Only a subset
of them, and you can’t tell exactly when — i.e. you just cannot assume anything about T. So
what are you going to write?
impl<T> Move for Creature<T> {
fn go(&mut self, direction: Direction) {
// …
}
}
This code will not compile, because you haven’t implemented Alive for Creature<T>. Remember that
the trait solver requires to prove super traits. However, and this is where all the interesting /
weird stuff happens:
impl<T> Move for Creature<T> where Self: Alive {
fn go(&mut self, direction: Direction) {
// …
}
}
This compiles. It compiles because the where clause tells rustc that your implementor will be
valid if used with Creature<T>: Alive. The distinction is really subtle, but is, to my opinion,
very powerful. With the where clause, you state that Move is implemented for any Creature<T>
that is also Alive, but you don’t require them all to be Alive! You could implement Alive
for a bunch of creatures, like Creature<Vampire> and Creature<CloudDog>.
So, I remember having read somewhere (maybe in some Rust book, but I’m not quite sure) that the
where Self: _ clause was not really useful, but in our case, you can see that it allows to
express a completely different semantics.
You could also have used
Creature<T>: Alivein place ofSelf: Alive, as here,Self = Creature<T>.
In Haskell, that code requires to use UndecidableInstances. I don’t know exactly why, but GHC
states that the constraint (Alive (Creature a)) is no smaller than the instance head
(Move (Creature a)), and this is not permitted, as being undecidable. Enabling the
UndecidableInstances GHC extension will make it possible to compile:
class Alive a where
getHealth :: a -> Health
-- The super class is declared on the left side of the => (the parenthesis are
-- optional, but I’m used to put them all the time, as they are required when you
-- have more constraints).
class (Alive a) => Move a where
go :: Direction -> a -> a
-- This instance requires UndecidableInstances to compile.
instance (Alive (Creature a)) => Move (Creature a) where
go = -- …
instance Alive (Creature Vampire) where
getHealth = -- …
instance Alive (Creature CloudDog) where
getHealth = -- …
I’m not quite sure why this very exact situation requires UndecidableInstances though. In this
case, it seems fine.
I hope you’ll have learned something with this small yet fun type theory candy. Keep the vibes!
]]>Although I didn’t add things nor write code at home, I thought a lot about graphics API designs.
APIs such as lambdacube or
GPipe are known to be graphics pure API. That means
you don’t have use functions bound to IO. You use some kind of
free monads or
an AST to represent the code that will run on
the target GPU. That pure design brings numerous advantages to us:
IO-bound, side-effects are reduced, which is good and improves the code
safety;Those advantages are nice, but there are also drawbacks:
So yeah, a pure graphics framework is very neat, but keep in mind there’s – so far – no proof it actually works, scales nor is usable for a decent high-performance for end-users. It’s the same dilemna as with Conal’s FRP: it’s nice, but we don’t really know whether it works “at large scale and in the real world”.
Most of the API out there are IO-bound. OpenGL is a famous C API known to be one of the worst one on the level of side-effects and global mutations. Trust me, it’s truly wrong. However, the pure API as mentioned above are based on those impure IO-bound APIs. So we couldn’t do much without them.
There are side effects that are not that bad. For instance, in OpenGL, creating a new buffer is a side-effect: it requires that the CPU tell the GPU “Hey buddy, please create a buffer with that data, and please give me back a handle to it!”. Then the GPU would reply ”No problem pal, here’s your handle!”. This side-effect don’t harm anyone, so we shouldn’t worry about it too much.
However, there are nasty side-effects, like binding resources to the OpenGL context.
So what are advantages of IO-bound designs? Well:
IO is the naked real-world monad;IO is the high-order kinded type of any application (think of the main function),
an IO API is simple to use in any kind of application;(MonadIO m) => m to add extra flexibility and create interesting constraints.And drawbacks:
IO is very opaque and is not referentially transparent;IO is a dangerous type in which no one has no warranty about what’s going on;Since the beginning, luminance has been an API built to be simple, type-safe and stateless.
Type-safe means that all objects you use belong to different type sets and cannot be mixed between
each other implicitely – you have to use explicit functions to do so, and it has to be meaningful.
For instance, you cannot create a buffer and state that the returned handle is a texture: the type
system forbids it, while in OpenGL, almost all objects are in the GLuint set. It’s very
confusing and you might end up passing a texture (GLuint) to a function expecting a framebuffer
(GLuint). Pretty bad right?
Stateless means that luminance has no state. You don’t have a huge context you have to bind stuff against to make it work. Everything is stored in the objects you use directly and specific context operations are translated into a different workflow so that performance are not destroyed – for instance luminance uses batch rendering so that it performs smart resource bindings.
Lately, I’ve been thinking of all of that. Either turn the API pure or leave it the way it is. I
started to implement a pure API using self-recursion. The idea is actually simple. Imagine this
GPU type and the once function:
import Control.Arrow ( (***), (&&&) )
import Data.Function ( fix )
newtype GPU f a = GPU { runGPU :: f (a,GPU f a) }
instance (Functor f) => Functor (GPU f) where
fmap f = GPU . fmap (f *** fmap f) . runGPU
instance (Applicative f) => Applicative (GPU f) where
pure x = fix $ \g -> GPU $ pure (x,g)
f <*> a = GPU $ (\(f',fn) (a',an) -> (f' a',fn <*> an)) <$> runGPU f <*> runGPU a
instance (Monad m) => Monad (GPU m) where
return = pure
x >>= f = GPU $ runGPU x >>= runGPU . f . fst
once :: (Applicative f) => f a -> GPU f a
once = GPU . fmap (id &&& pure)
We can then build pure values that will have a side-effect for resource acquisition and then hold
the same value for ever with the once function:
let buffer = once createBufferIO
-- later, in IO
(_,buffer2) <- runGPU buffer
Above, the type of buffer and buffer2 is GPU IO Buffer. The first call runGPU buffer will
execute the once function, calling the createBufferIO IO function and will return buffer2,
which just stores a pure Buffer.
Self-recursion is great to implement local states like that and I advise having a look at the
Auto type. You can also read my article on netwire, which uses self-recursion a lot.
However, I kinda think that a library should have well defined responsibilities, and building such
a pure interface is not the responsibility of luminance because we can have type-safety and a
stateless API without wrapping everything in that GPU type. I think that if we want such a pure
type, we should add it later on, in a 3D engine or a dedicated framework – and that’s actually what
I do for demoscene purposes in another, ultra secret project. ;)
The cool thing with luminance using MonadIO is the fact that it’s very easy to use in any kind
of type that developpers want to use in their applications. I really don’t like frameworks which
purpose is clearly not flow control that actually enforce flow control and wrapping types! I don’t
want to end up with a Luminance type or LuminanceApplication type. It should be simple to use
and seamless.
I actually start to think that I did too much about that pure API design idea. The most important part of luminance should be type-safety and statelessness. If one wants a pure API, then they should use FRP frameworks or write their own stuff – with free monads for instance, and it’s actually funny to build!
The next big steps for luminance will be to clean the uniform interfaces which is a bit inconsistent and unfriendly to use with render commands. I’ll let you know.
]]>It’s been a while I’ve been wanting to do that. Now it’s done! smoothie, a Haskell package for dealing with curves and splines, updated to version 0.3.
That version introduces several changes. If you’re a good programmer, you might already have noticed that the major version got incremented. That means there’re compatibility breaking changes. If you don’t know what I’m talking about, you should definitely read this.
The first – non-breaking – change is that the package now supports Bézier interpolation! I’ve been reading about Bézier curves for a while because there’re very present and important for animation purposes – think of Blender. Feel free to dig in the documentation on hackage for further details.
The second – breaking – change is that the interface has changed, especially the implementation of splines. However, the interface is now simpler and doesn’t require a lot of change in your code if you’ve been using older versions.
Feel free to read the CHANGELOG for technical hints.
As always, tell me what you think of the library, and keep the vibe!
]]>You can’t even imagine how hard it was to release luminance-0.7. I came accross several difficulties I had to spend a lot of time on but finally, here it is. I made a lot of changes for that very special release, and I have a lot to say about it!
As for all my projects, I always provide people with a changelog. The 0.7 release is a major release (read as: it was a major increment). I think it’s good to tell people what’s new, but it should be mandatory to warn them about what has changed so that they can directly jump to their code and spot the uses of the deprecated / changed interface.
Anyway, you’ll find patch, minor and major changes in luminance-0.7. I’ll describe them in order.
A lot of code was reviewed internally. You don’t have to worry about that. However, there’s a new
cool thing that was added internally. It could have been marked as a minor change but it’s not
supposed to be used by common people – you can use it via a flag if you use cabal or stack
though. It’s about debugging the OpenGL part used in luminance. You shouldn’t have to use it but
it could be interesting if you spot a bug someday. Anyway, you can enable it with the flag
debug-gl.
The UBO system was buggy and was fixed. You might experience issue with them though. I spotted a bug and reported it – you can find the bug report here. That bug is not Haskell related and is related to the i915 Intel driver.
The minor changes were the most important part of luminance-0.7. luminance now officially supports
OpenGL 3.2! When installing luminance, you default to the gl32 backend. You can select the
backend you want with flags – gl45 and gl45-bindless-textures – but keep in mind you need the
appropriate hardware to be able to use it. Because you need to use flags, you won’t be able to
switch to the backend you want at runtime – that’s not the purpose of such a change though.
The performance gap should be minor between gl32 and gl45 but still. Basically, OpenGL 4.5 adds
the support for DSA, which is very handy and less
ill-designed that previous iterations of OpenGL. So a lot of code had to be rewritten to implement
luminance’s stateless interface without breaking performance nor bring them down.
I might add support for other backends later on – like an OpenGL ES backend and WebGL one – but that won’t ship that soon though because I have a ton of work to do, and yet need to provide you with a concrete, beautiful, fast, appealing and eye-blowing demo with luminance! ;)
Feel free to test the gl32 backend and give me back feedback!
However, if I spent so much time on that 0.7 version, it’s because I had issue whilst writing the
gl32 backend. Indeed, I spotted several bugs on my Intel HD card. This is my OpenGL version string
for my Intel IGP card:
OpenGL core profile version string: 3.3 (Core Profile) Mesa 11.0.4
The architecture is Haswell. And on such a card (i915 linux driver) I’ve found two bugs while
trying the gl32 backend with luminance-samples-0.7.
usampler2DFor unknown reason, the Texture sample failed on my Intel IGP but ran smoothly and flawlessly on
my nVidia GPU. I spent a lot of time trying to figure out what I was missing, but eventually changed
the sampler type – it’s now a sampler2D – and… it worked. I reported the issue to the intel dev
team. So if you hit that error too, please leave a message here so that I can have more hindsight
about that error and see what I can do.
vec3This is a very nasty issue that kept me awoken for days trying to fix my code while it was a driver bug. It’s a big technical, so I’ll just leave a link to the bug tracker so that you can read it if you want to.
Ok, let’s talk.
When creating a new shader stage, you now have to use the function createStage – instead of
several functions like createVertexShader. That change is important so that I can add new shader
types without changing the interface, and because some shader can fail to be created. For instance,
on the gl32 backend, trying to build a tessellation shader will raise an error.
When a shader stage creation fails, the UnsupportedStage error is raised and holds the type of the
stage that failed.
Finally, the interface for the cubemaps changed a bit – you don’t have access to width and height anymore, that was error-prone and useless; you’re stick to a size parameter.
I’d like to thank all people supporting me and luminance. I’ll be watching reactions to that major and important release as it will cover more people with cheaper and well-spread GPUs.
Happy hacking! :)
]]>glsl_str! proc-macro would have only survived the 0.1 version. It’s now deprecated and
will be removed soon.glsl! proc-macro now supports GLSL pragmas (both #version and #extension).“Wait. GLSL pragmas? You said it wasn’t possible because of how the Rust tokenizer thinks newlines are meaningless while the GLSL grammar uses them to separate pragmas from other pragmas and code.”
Yes, at the time of writing glsl-quasiquote, I just thought that since TokenStream is about a
stream of Rust tokens, GLSL’s pragmas would be impossible to encode with this mechanism. In fact,
it’s actually impossible per-se: newlines are not Rust tokens and are just completely ignored by the
tokenizer… or are they?
At the announcement of the release of glsl-quasiquote on Reddit, someone – actually,
somebodddy just
pointed out that I could use Rust attributes to encode GLSL pragmas. That would enable me to have
them as regular Rust tokens at the cost of modifying very, very briefly the GLSL grammar for the
pragmas – nothing bad, really. Their idea, which I liked, was this:
Instead of:
#version 330 core
We could write:
#![version 330 core]
Such a small change yet effective, right? I was appealed by the idea, so I implemented it. The code was a bit messy because I did all the token pattern-matching by hand but I sketched something up that worked in a few minutes. After having a discussion with a friend on IRC (antoyo!), I realized that I was plain wrong from the beginning assertion that newlines are ignored by the Rust tokenizer: they’re not.
The proc_macro crate, which is used to manipulate Rust tokens, has several items that I used
to implement the quasiquoter:
TokenStream, a stream of tokens. This is a very opaque type, you can just create an empty one,
display it as a String and marshall to and from an iterator interface, which element type is
TokenTree.TokenTree, a single token or a delimited sequence of token trees, like a group of tokens
logically united (with brackets, parens, etc.).TokenTree variants, such as Literal, Punct, Group, etc.There’s something I just had completely forgotten when claiming that newlines were ignored: pretty
much all of the tokens contained in TokenTree have an associated Span. I’m not sure what the
initial intent was about with this type, but in order to use it, I had to add the proc_macro_span
feature. That type gives positional information along with macro expansion on a token. Specifically,
it has a method that gives the line and column at which the token starts and ends.
This information gives us the line on which a token lays. That’s it. We have the newlines! A newline is just whenever a token following another token has a different line in its span. That’s as easy as it gets.
So instead of implementing GLSL pragmas via Rust attributes, I decided to implement them the way
they are defined in the spec and the glsl crate: plain GLSL. The idea was just to match a # and
accumulate tokens in a collection as long as tokens lay on the same line. As soon as the line
changes, we have the full pragma and we can call TokenStream::from_iter.
The current implementation expects the pragmas to be at the top part of the quasiquoted GLSL code.
You shouldn’t be trying to put the #version at the bottom of your file, but I wanted you to be
noticed: don’t do that!
Since glsl-quasiquote-0.2, this Rust code now completely compiles and generates, at compile-time, a GLSL AST:
let ast = glsl!{
#version 330 core
#extension GL_ARB_separate_shader_objects : require
void main() {
}
};
Some future announcements to come (things that I’ve already been working on and that I might release soon):
]]>In this blog entry, I want to talk about UX more specifically, because I do think it’s more important than UI. I will try to show that the default UX of Kakoune is incredible and that you can very quickly create a super expressive and pragmatic programmer environment. It will cover:
Once again, I’d like to greet and thank @Taupiqueur for sharing his thoughts and joy about Kakoune.
UI means “anything interfacing the user to the system.” It’s both the visual depiction of the service (the menu, the colors, the fonts, etc.) and the way you interact with the system (with the keyboard, by clicking on buttons, when a system event happens, etc.).
UX means “how the user experiences the system.” For instance, something that is not UI at all but enhances the UX is having a way to filter data in a system with high volume of events with a tag. An even better UX is having fuzzy filtering with any tags. Etc.
There are many possible UI implementations for a given UX item: implementing filtering can be done with a select box in a GUI, but the UX is not great because the user is presented with a set of finite choices, and if there are many, it’s pretty annoying to scroll down the list to find the one we want. Even with this bad UX design, most of select box implementations (e.g. web) allow to press keys to jump to the entry in the select options — most of the time, you don’t see what you type -> bad UX for the user. Another possible UI implementation would be to use a free text box that would filter based on its content — it could even be live for an even better UX. Etc. etc.
Now, would you prefer a nice looking GUI with the select box, or a blander UI but with the fuzzy search box? Clearly, in terms of UX, the second option is much better. But now, imagine a GUI with the fuzzy search box. It would be pretty similar to the blander UI in terms of UX, but it would look (much) better… which is likely to enhance the UX as well!
TUI: Text User Interface, which is a program mimicking traditional GUIs in the terminal.
GUI: Graphics User Interface, the typical window-based applications you run on your machine.
Before jumping to the Kakoune content, I just want to disgress slightly to talk about something I often see something that itches me a bit: many individuals seem to say that a TUI is often written in a way that optimizes UX and GUI the UI, and hence, oftentimes, using a TUI feels much better. I agree with this (this is the reason why I’m using editors and tools inside my terminal instead of a dedicated GUI, even though I think the UI is worse, for many reasons: no pixel-perfect alignment of things; no direct integration of the application at the OS level, it has to go through the terminal and shell; etc. etc.).
However, is there anything forcing a GUI not to provide the same kind of interactions as a TUI? I don’t think so. If you make your TUI keyboard-oriented, everything you do is just listening to keyboard events provided by whatever terminal protocol / library you use… which could be done exactly the same way in a GUI.
I do think that we should be able to make a GUI as good as a TUI in terms of UX, and some programs did it. For instance, emacs can run both as a TUI and a GUI (and today, people recommend actually using the GUI). I think that could be the topic of another blog article. Let’s go back to the original matter.
Kakoune, upon installation, already provides a lot of good things in terms of UX. But as you get more productive with it, you will face some problems. For instance, the first one I came across (pretty quickly being honest, coming from Helix) was surrounding: adding, deleting and replacing surrounding delimiters. Where you need a plugin to do that in the Vim world, in Kakoune, it’s another topic. Some plugins exist to do exactly that, but honestly, read along.
Selections in Kakoune — which are so much more than “iT’s JUsT LikE muLtIcURsOrs oR a nOOb WaY Of DoINg RegEX in
vIM” — change the Vim interpretation of appending and preppending. In Vim, i will start inserting on the cursor,
whereas a will start inserting after the cursor. In Kakoune, i inserts at the beginning of each selection and
a inserts at the end of each selection. That’s already a better UX, and it allows to do many things out of the
box.
For instance, since we have the power to insert at the beginning and at the end of each selection… then pressing i'
should insert a quote at the beginning of the selection… and a' will do the same at the end! So you can already have
a somewhat working surrounding add operation by typing i'<esc>a'!
If you are adventurous, you can read the documentation of
<a-;>, which allows to leave insert mode to normal mode for a single command, and come back to insert mode. You can use this to replicate what we did above without the<esc>key:i'<a-;>a'. Magic.
It’s a bit annoying to have to type all that, though, right? So instead, we could make a command! Commands in Kakoune are really simple, but they require reading a bit about them. I recommend the following reads if you want to dig in a bit:
:doc commands to know about all the commands. Search for ^define-command.:doc execeval, which explains what evaluate-commands and execute-keys do. Especially, you will want to read the
part of -draft.So let’s wrap that sequence of keys in a command. Instead of using i and a, we are going to use P and p, which,
as the name implies, paste from the default register. p pastes at the end of the each selection and P pastes
at the beginning of each selection.
What is great is that the default register, ", is selection-aware: its content will be different from one selection to
another. Said otherwise, there is one " register for each selection. Hence, we can write this:
define-command -override my-surround-add -params 2 %{
evaluate-commands -draft -save-regs '"' %{
set-register '"' %arg{1}
execute-keys -draft P
set-register '"' %arg{2}
execute-keys -draft p
}
}
And here you go. You can now invoke :my-surround-add ( )<ret> to add parenthesis around your selections, for instance.
Kakoune has the concept of user modes, which is a nice feature allowing to declare a keyset (that will be displayed by the help in the bottom right of your screen) when entered.
declare-user-mode my-surround-add
We can make one with a bunch of mappings in that user mode:
map global my-surround-add ( ':my-surround-add ( )<ret>' -docstring 'surround with parenthesis'
map global my-surround-add ) ':my-surround-add ( )<ret>' -docstring 'surround with parenthesis'
map global my-surround-add [ ':my-surround-add [ ]<ret>' -docstring 'surround with brackets'
map global my-surround-add ] ':my-surround-add [ ]<ret>' -docstring 'surround with brackets'
map global my-surround-add { ':my-surround-add { }<ret>' -docstring 'surround with curly brackets'
map global my-surround-add } ':my-surround-add { }<ret>' -docstring 'surround with curly brackets'
map global my-surround-add < ':my-surround-add < ><ret>' -docstring 'surround with angle brackets'
map global my-surround-add > ':my-surround-add < ><ret>' -docstring 'surround with angle brackets'
map global my-surround-add "'" ":my-surround-add ""'"" ""'""<ret>" -docstring 'surround with quotes'
map global my-surround-add '"' ":my-surround-add '""' '""'<ret>" -docstring 'surround with double quotes'
map global my-surround-add * ':my-surround-add * *<ret>' -docstring 'surround with asteriks'
map global my-surround-add _ ':my-surround-add _ _<ret>' -docstring 'surround with undescores'
Obviously, that’s not all; we would need to delete delimiters and to replace them. Deleting is actually even more straightforward. Kakoune has some native mappings to select everything inside or around a set of delimiters:
<a-i> to select inside.<a-a> to select outside.So, pressing <a-a>( (or <a-a>), same thing) will select everything around the cursor up to the next pair of
parenthesis.
Note: I have personally remapped that to
miandma, but that collides with the default meaning of themkey in Kakoune, so I will use the native mapping here instead.
We can then simply use the previous i and a command mentioned before to start editing at the beginning and end of
the selection. i<del> will start insert mode at the beginning of the selection and will remove a character (left
delimiter) and a<backspace> will insert at the end of the selection and remove the previous character (right
delimiter). Eh, that looks like so simple it’s almost stupid. But that’s what makes Kakoune so damn good: it’s simple
to reason about:
define-command -hidden my-surround-delete-key -params 1 %{
execute-keys -draft "<a-a>%arg{1}i<del><esc>a<backspace><esc>"
}
define-command my-surround-delete %{
on-key %{
my-surround-delete-key %val{key}
}
}
Because Kakoune composes really well, you can already imagine that you should be able to use the previous commands and mappings. And indeed:
define-command my-surround-replace %{
on-key %{
surround-replace-sub %val{key}
}
}
define-command -hidden my-surround-replace-sub -params 1 %{
on-key %{
evaluate-commands -no-hooks -draft %{
execute-keys "<a-a>%arg{1}"
# select the surrounding pair and add the new one around it
enter-user-mode my-surround-add
execute-keys %val{key}
}
# delete the old one
my-surround-delete-key %arg{1}
}
}
Ah… who has never had the problem of trying to locate something in a codebase without really knowing where to start.
That happens a lot to me when working on a front-end project and taking an issue asking to fix a random page, that is
broken. Usually, LSPs don’t help to discover things that are based on the final product. For instance, if you see the
checkout page broken — like a <div> is missing an attribute or a tag is misplaced in the DOM, no LSP will help you
locate the code that needs to be fixed. Instead, you need other tools.
What I like to do is looking at the page and looking for what I call unique tokens. For instance, a short sentence
that might appear only on that page, or modal. Or a header, a title, etc. Then, using that information, I usually grep
the code base. The problem is that, doing that using grep or ripgrep as CLI is pretty boring, and not very
practical. Indeed, if you get many results, you are likely to try to reduce the result set by constraing more your
regex. Once you have some files, you usually look in your terminal scrollback buffer until you find something
interesting, then open that file in your editor.
People using something like IntelliJ products, or VS Code, or some plugins with emacs or vim might have a way to perform the search from within their editors, but again, that’s not composability: it’s extensibility, and I explained in a previous article why it’s not a good design to me.
Kakoune, on the other side, went the composition route. grep, ripgrep, etc. are all amazing tools.
Kakoune comes with a bunch of what it calls tools, which are basically Kakoune commands shipped with the editor. Among
those, there is one that is of interest here: the :grep command. The :grep command forwards its arguments to the
underlying %opt{grepcmd} (which defaults to something like grep -RHn). Hence, :grep foo will run grep -RHn foo
in a shell behind the scene, then the result will be output in a *grep* buffer. That buffer will get special
highlightings, along with some keybindings, all of that provided by the grep.kak tool. If tried the command, you might
have noticed that it’s basically a list of lines of the form:
<path>:<line>:<column>:<content-of-the-line>
Then, how do you think grep.kak implements _pressing <ret> jumps to the line, column and file of the line under the
cursor? The cursor can be anywhere on the line. Well, it’s simple: execute-keys again! gh will put your cursor at
the beginning of the line. Then, you can use f: or t: to jump to the next : — there is no need to parse anything,
we can just programmatically interact with the editor!
Here, ghT:"py will go the beginning of the line, select the path and yank it to the p register. We can then do
2l to move the cursor to beginning of the <line> number, and T:"ly will yank the line number to the l register.
2l again to move to the <column> section, ten T:"cy to yank the column number to the c register. And we have
everything we need. We can then just simply run the edit -existing %reg{p} %reg{l} %reg{c} command:
aoc-18.md:1:1:lol
define-command -override my-jump-for-current-line %{
evaluate-commands -save-regs clp %{
execute-keys -draft 'ghT:"py2lT:"ly2lT:"cy'
edit -existing %reg{p} %reg{l} %reg{c}
}
}
A couple of comments here:
-save-regs clp saves the content of the c, l and p register before evaluating the commands, then restores
those registers afterwards. That is required since we copy a bunch of data to those registers, but maybe the user is
already using them. -save-regs allows us to specificy a set of registers we locally need, but we do not want those
registers to bleed outside.-draft makes the command evaluation run in disposable context. That prevents our selections from being changed —
execute-keys runs a couple of move and goto commands here, while we don’t really want the cursor to move.-existing fails if the file doesn’t exist.The :grep tool implements something probably very similar to this, but it uses %opt{grepcmd}, which you can change
to whatever you like. I personally use:
set-option global grepcmd 'rg --column --smart-case --sort path'
Something that is very important having around is being able to locate files very quickly. Most editors today ship with a way to locate files:
Kakoune has a default powerful completion engine. Pressin :edit (or, for short, :e ) and then typing something will
auto-complete the file in the current directory with a somewhat fuzzy algorithm. If you select a directory and type the
trailing /, it will list the content of that directory and will auto complete its children. It’s a pretty nice way to
start moving around with the vanilla editor.
However, Kakoune doesn’t ship more than this, because, well, it’s composable. You can use any tool you like to locate
files, and then compose them with Kakoune. I personally really like fd (a Rust rewrite of find). For instance,
fd --type file will locate all the files in the current directory. Piping that to a fuzzy finder would allow to
jump to any file in the current directory. And Kakoune supports that. It has several commands for that, but the one you
will be interested at first is prompt. It prompts the user for some text and pass the entered text to the provided
commands in the %val{text} variable. It supports some switches, and one of interest for us is
-shell-script-candidates. That switch accepts a shell command to execute, reading from its standard output
asynchronously and displaying into the completion items. For instance, try running the following command:
prompt -shell-script-candidates ls file: %{ info "you selected %val{text}" }
It runs ls, gets the output and allows you to fuzzy complete the result. Now consider this:
prompt -shell-script-candidates 'fd --type file' open: %{ edit -existing %val{text} }
And bam. Here you have it. Fuzzy picker in Kakoune. You can map that to a command, or wrap it in a command:
define-command my-file-picker %{
prompt -shell-script-candidates 'fd --type file' open: %{ edit -existing %val{text} }
}
If — like many people — you want that to be run when you type SPC f:
map global user f ':my-file-picker<ret>'
You can do the same thing with anything, really. Some plugins like kak-lsp uses that to show document symbols, etc.
By default, Kakoune comes up with registers you can yank to. However, oftentimes, you need to yank and paste from the system cliboard. Kakoune doesn’t have a “system register” as in Vim. Instead, as with the rest, you have to compose some tools with Kakoune to get that feature. And it’s pretty simple. You need to know how to yank some text in CLI. Two situations:
pbcopy and pbpaste to respectively copy and paste from/to the system clipboard.xclip and xsel -ob commands to do the same.And then, there is nothing much more to do. We can wrap those in two commands agnostic of the platform by using the
uname utility:
declare-option str extra_yank_system_clipboard_cmd %sh{
test "$(uname)" = "Darwin" && echo 'pbcopy' || echo 'xclip'
}
declare-option str extra_paste_system_clipboard_cmd %sh{
test "$(uname)" = "Darwin" && echo 'pbpaste' || echo 'xsel -ob'
}
And the actual commands, using the ! key (to run a shell command with the current selection piped as standard input
and replace selections with its output) and <a-!> (which runs a shell command and ignore its output):
define-command extra-yank-system -docstring 'yank into the system clipboard' %{
execute-keys -draft "<a-!>%opt{extra_yank_system_clipboard_cmd}<ret>"
}
define-command extra-paste-system -docstring 'paste from the system clipboard' %{
execute-keys -draft "!%opt{extra_paste_system_clipboard_cmd}<ret>"
}
I personally map those as in Helix:
map global user y ':extra-yank-system<ret>' -docstring 'yank to system clipboard'
map global user p ':extra-paste-system<ret>' -docstring 'paste selections from system clipboard'
Because the UX in Kakoune is amazing, and because it composes so well, I have made a couple of binaries and Kakoune commands to enhance the experience. So far (23rd December of 2023), I have wrote those
- TODO, - WIP, - DONE, - WONTDO, - IDEA, etc. are highlighted with specific faces.<ret> jump to the line under the cursor (or <esc> goes back to where you were buffer). I
also included a reduce mode that helps with reducing grep buffers or any buffer that can be directly modified.All in all, I’m really happy with my current Kakoune setup, and as time passes, I realize it’s a nice interactive
editing platform so far. For instance, at work, I have started making a k8s.kak to highlight and run commands on
Kubernetes cluster from within Kakoune; and it works pretty well.
Have fun and keep hacking around!
]]>
I want to talk about it, because it’s not really the type of keyboard you see quite often, and I guess it comes with many questions.
The first thing you might have noticed is that it’s a split keyboard. There are a lot of litterature lying around explaining why it’s actually a better setup than a single piece of hardware — better shoulds position, better wrists alignment, etc. Plus, because you have the choice to put both sides close to each other, or far apart, it can easily be adapted to your morphology.
Secondly, it’s easy to see that the Corne has only 42 keys. That comes with a massive advantage to me: when your fingers are resting on the home row, every key on the main grid (i.e. excluding thumb keys) are only one key away. That minimizes a lot the movements you will be doing. For instance, reaching the key on the top-left corner doesn’t require any kind of stretch of my hand, nor rotation. It’s already an improvement compared to the Voyager, which has an additional row above; reaching its top-left key does either make you extend your pinky in a non-natural way, or does make your wrist slightly rotate towards the keyboard, which is not comfortable on the long run.
So every key is reachable by just slightly moving the fingers. It’s a fantastic feeling, especially because of the column staggered keys, which follows the curve of the hands. I’ve been touch typing for a very long time now (> 10 years), so switching to that keyboard wasn’t that hard — especially because I had switched to the Voyager prior to this — but it would a lie to tell it’s natural. It takes some time and practice. If you have never learned to touch type properly, you might suffer a lot. I’ve been touch typing in bépo, so it took me a couple days to be fully comfortable with the Voyager and the Corne.
Finally, I want to talk about the thumb keys, because it’s going to be a central topic of this article. The Corne has three on each side, for a total of six, which is still two more than the Voyager, which has two on each side for a total of four. Thumb keys are a way to leverage the fact that on regular keyboards, we barely use them — on my 60%, I used the left thumb for the space bar, and the right thumb for alt-gr, but that’s pretty much it, besides some occasional modifiers.
So that’s it for the introduction. Let’s dig into my setup.
Because of the few amount of keys, the whole idea is that you want to create layers, and dedicate some keys to activate modifiers and layers. Let me explain.
The base layer is usually full of letters, what you will be typing the most. In my case, this is my base
layer — · means that nothing is bound there:
T b é p o è ^ v d l j z
E a u i e , c t s r n m
% à y x . k ' q g h f w
1 Z · B R ·
Here, T stands for tab, E is escape, Z is space, S is left shift, B is a backspace, R is return.
Some keys have a hold action, which is what we usually call mod-tap: holding the key down and pressing
another key does something different. Even though some keys do not have any tap action (for instance, the
left inner thumb key or the right outer thumb key), I can still hold them to have a specific action.
Here’s the base layer again, but with the actions when keys are held:
M · · · · · A · · · · ·
· · · · · · · · · · · ·
A C · · · · · · · · · ·
2 · S S 1 G
M is meta, A is alt, C is control, S is shift, and G is alt-gr. That latter modifier is especially
important in bépo, as it opens up many many characters that the bépo layout has to offer — for instance, …
is alt-gr + ., ’ is alt-gr +, œ is alt-gr + o, etc.
The 1 and 2 are not typos, and I’ll talk about it pretty soon.
You might have noticed that I have mirrored the shifts on the inner thumb keys. This is important to me, because shift is actually a very important modifier: it’s by far the modifier I type the most, and I need to be able to interleave normal typing on the main grid with shifting modifiers. Think about the sentences you’re currently reading, and where I might have pressed shift. « I » in the middle of a sentence requires a shifted « i ». Every start of sentence requires a shifted letter.
Now let’s see how I type numbers and symbols.
I used to have this layer mirrored on the outer thumb keys, but such keys are not really practical to hit
while fast typing — I usually type around 145 words per minute, so I’m pretty fast. Today, that layer is
accessed via the 1 layer key on my base layer — middle right thumb key. If I just tap it — basically press
and release without touching any other key — it acts as return. If I hold it, it activates the symbols and
numbers layer.
Let’s see that layer:
0 1 2 3 4 5 6 7 8 9 ° ·
$ " < ( ) > @ + - / * =
# | & [ ] ~ — { } « » ç
· _ S · □ ·
The □ is the held key there. So, if I want to type (), I can simply hold the right middle thumb key and
press ie to yield (). How to do ->? Easy: press the layer key, and s,. The placement of the keys
are easy to remember.
One that is very important to me is
_: that one is usually accessed viaalt-gr + <space>, but because my alt-gr is on the right outer thumb key, it’s not super practical to type. However, I also realize I don’t type underscore that often, and that I might put backspace there instead. This is still something I’m figuring out.
This is a pretty fantastic layer, because everything is really easy to activate. The weirdest key is
definitely —, since the right index and thumb fingers are super close to each other — but it’s a fair
tradeoff as I very rarely type this key, but still require it.
The
çis an anomaly due to bépo v1.0, which doesn’t treatalt-gr + casç, but as©, which I do not really care. I need to switch to bépo v1.1 / AFNOR, which comes with issues with'vs.’; yes, French is hard!
You can see that I moved 0 on the left side; this is to prevent having to press it on the right side with a
weird crab hand; 9 is fine, but 0 would be too weird on the right side.
Next up is my navigation layer, accessible with the 2 key.
That layer is important for many different softwares, including shells, chat applications, etc. It’s the layer that holds arrow keys, page up / down, home / end, etc. Because it’s also a pretty empty layer, I hijack it to also hold useful multimedia keys, such as volume up, down, mute, and the print key:
3 · · · · VU P H U E · ·
· · · · · VD l d u r · ·
· · · · · VM · · D · · ·
· · · S · ·
3 is my gaming layer goto-key (more on that later), VU, VD and VM are volume up, volume down and
volume mute; P is the print key, H and E are home and end keys, U and D are page up / down, and
l, d, u and r are the left, down, up and right arrow keys.
Yes, the volume keys are not optimally placed, and I might move them on the right side eventually. The reason is that since the nav key is pressed on the left outer thumb key, and is not practical to type keys on the left side of the keyboard.
Alright, now let’s see the last layer I have defined:
The gaming layere is a bit weird and heavily optimized towards the games I play — mostly, Quake, or games with a need of special keys all around my movement keys:
T è b é p o · · · · · 0
E , a u i e · · · · · ·
% à y x . · · · · · ·
k Z S · · ·
0 here goes back to the base layer. As you can see, the layout is weird, but the idea is that it will make
most games work out of the box with ASDF key, but I shift them by 1 column to have the direction keys right
under the fingers without moving from the left home row. I don’t think there’s anything to debate here, it’s
just a shifted base layer.
Something that I wondered a lot is whether I want to use HRMs. HRMs assigns hold-actions on the four keys of the resting home row position to control, alt, shift and meta, on each side of the keyboard. That allows to have access to modifiers without moving your fingers. This, basically:
· · · · · · · · · · · ·
· M A C S · · S C A M ·
· · · · · · · · · · · ·
· · · · · ·
I is smart, but not practical, at least not to me. The idea is that it relies heavily on how it’s
implemented. I use QMK to build my firmwares, and when you use HRMs, there is a setting you cannot really
do without: permissive-hold. In short, permissive-hold changes
the behiavor of mod-tap keys in order to active the hold action more often. By default, to get the hold action
of a key, you have to press it for more than TAPPING_TERM — usually set to 200ms. With permissive-hold,
the hold action can be triggered sooner by pressing the mod-tap key and pressing and releasing an other key,
while still holding the mod-tap key. Imagine a mod-tap key that prints t if tapped, but right shift if
held. With permissive-hold, you can trigger the hold action without having to wait for TAPPING_TERM:
t key.a key for instance.a key.A printed.t key.This is required if you want to use HRMs and still trigger hold actions without having to wait for
TAPPING_TERM. The other mode — which I use, and I’ll explain why — is named hold-on-other-key-presses,
which changes the behavior of mod-tap keys like so:
t key.a key.A is printed.t key.a key.This is usually how I type when typing fast, so that mode is really horrible as it generates tons of misfires with HRMs. However, it’s the mode to use for mod-taps set on keys you rarely use doing rollovers, such as my backspace key which is also my right shift: I usually tap it once or twice, without holding on any other key. Whenever I press it and hit another key, 100% of the time, I wanted a shifted key.
So having the shifts on the thumbs and hold-on-other-key-presses allows me to shift keys immediately, which is really important while shifting keys in the middle of sentence or words.
But there’s more. I really tried HRMs — switched to permissive-hold, and removed the shifts from the thumbs. I basically have two big issues with it.
Because shifts are really special and important keys, having them on the home row just gets in the way too much. You don’t realize it until you have to type complex sentences with tons of shifting, but I realized that I often start shifting a character, and my fingers are already moving to the next digram or trigram. Having two fingers forced to ping-pong between shifts, and because you cannot press them quickly (remember: permissive-hold requires you to press a key, and press and release another key before releasing that first key first to get she hold action! this is not how I fast touch type, so I really feel slowed down with this setup).
This issue also outlines a misconception — or a paradox, pick the word that pleases you the most there —
about symmetrical shifts. Whenever I need to shift a single character, I will pick the shift key on the
opposite hand. For instance, « I » is an i, which I type on the left side on the keyboard, and thus I shift
it mith the right middle thumb key held: « I ». However, some words sometimes require to shift a couple
keys one after the other. For instance, there’s a city in France called Anet. Let’s yell at that city, for
whatever reason: ANET!
The way I type ANET is by following the opposite-hand rule I mentioned above for the first letter, but then I just keep the shift key held and finish the rest of the word. It looks like this:
a.n.e.t.Now, with HRMs, you cannot do that, because every letter of anet alternates between sides, this is what it would look like:
t.a.a.t.e.n.n.e.t.e.e.t.e.t.t.e.Yeah. It’s crazy. And here, I really took the worst example, but in practice, that kind of alternating pattern happens more often than you’d think. For this special reason, I think shifts should be treated as a special key, and that HRMs should not include shifts.
So this one is probably the most infamous one coming with HRMs, and either you just don’t care, or you’re like me and can’t stand it. I’ve heard and read people stating they do not feel it, but on my side, it’s very similar as playing at 30 FPS vs. playing at 244 FPS: I just do not comprehend how someone cannot feel the difference.
What happens with HRMs introducing latency is easy to understand, and it’s about the taps, not the hold action. With permissive-hold, as seen above, if you press and release another key while holding down a mod-tap key, you will get the hold action. If you fast type, and have the interleaving of press / release, you will get taps, which is fine, but. Because they mod-tap key might be a shift key if hold, the firmware must wait until the end of the tapping period, or that the key is released to actually consider it a tap. What it means if that, if you press a key and release it, the associated character will not immediately appear on screen: it will take the time between the moment you press the key and the moment you release it. The character will be printed on screen when you release the key.
You can state that the difference in time between a press and the associated release is negligeable for taps, right? Right? Well, not really. Even when I type really fast, I cannot easily go under 60ms on single taps, and rarely under 40ms for super fast burts.
A simple and KISS way to check this out: open your web browser console, and add this JavaScript to any page:
document.onkeypress = () => { down = window.performance.now(); }
document.onkeyup = () => { console.log(window.performance.now() - down); }
Yeah it’s terrible code but who cares for such a test. Just simply tap a single key the way you normally type. This is just there to show you that with HRMs, you will get most of the time around 60ms of latency if you are a fast typer, probably much more for most of the people.
Just for comparison, for a game to run at 30 FPS, it must compute every frame with a maximum render time
boundary of 1/30 ~= 33ms. If a game were taking 70ms to render each frame, it would run at 14 FPS. So this
is akin to playing a game at full FPS, like 240 FPS, but having your mouse and keyboard input at 14 Hz. I
don’t think you’d like that. So yes, HRMs introduce a very noticeable lag, and I cannot bear it. It feels
terrible. Something that people also seem to ignore is that the human brain is pretty good at making
correlation between a sensation and what they eyes see. For instance, if you have ever watched a movie with
out-of-sync video and audio, you might have realized how quickly you can notice that something is wrong: just
having a couple milliseconds of delay between what you see and what you hear already feels bad.
Fun fact: because the speed of light is the top-speed, and the speed of sound is pretty slow compared to light, desynchronizing audio and video will not feed as bad if the audio arrives after the video; think thunderstrucks for instance. But your brain will freak out if audio arrives before.
Contrary to what many people will say, this is an unsolvable problem. There are some options to try to mitigate the problem — for instance, Flow Tap — but those still introduce tradeoffs that are not good ones to me. Flow Tap introduces a timeout after which tapping a key will disable HRMs, so you can speed through without having the latency. However, it doesn’t solve the problem in all situations, and I still think that permissive-hold is not as good as hold-on-other-key-presses on keys you don’t often use.
I’m considering changing a bit my layers, especially regarding backspace, return, etc. All in all, I think that having so few keys does require some tradeoffs, and I think I’ve found the perfect setup for my personal use. Who knows, maybe some day I will find a revolutionary way to implement HRM and will switch to them, but for now, I don’t think it’s actually that good.
It was a lighter topic, but still pretty fun to talk about it. In the end, I just love keyboards.
Keep the vibes!
]]>My take on all of that has evolved a bit over the last months / weeks. A couple of things happened and I think it’s time for an update.
If you have read my articles about development environment, you already know what I’m talking about here. If not, let me do a little summary. Basically, as a software engineer, I need to use tools to solve problems. Those problems are daily little issues while working on a project:
bazel, helm, kubernetes, etc. and composing them with the rest.Whatever the tools you are using, those problems will roughly be the same. Maybe you don’t have them all, but you will come across a couple of them on a daily basis. Let’s start talking about what most people use: IDEs.
An IDE (Integrated Development Environment) is a software that provides a solution to a subset of the problems I mentioned above. For instance, IntelliJ IDEA has a solution to edit your code, go to definitions, implementations, lookup files, classes, symbols; version your code and write Git commit messages; it has a debugger; it has a way to build and test your code; it has a terminal so you can run random CLI commands; etc. It doesn’t solve all the problems, but clearly it solves a subset of them. With programs like that, most of the time, you install it, and you’re good to go without configuring anything. JetBrains editors are famous (and loved!) for this reason. You want to work with Java? Install IDEA. Of course, you will still find plugins to customize the experience, but the vanilla editor is most of the time enough.
Then you have “customizable” IDEs, such as VS Code. Such softwares will often require you to download plugins because the default experience is unlikely to have support for your language, even though it should be enough for most people. VS Code is clearly the most famous one and it has a plugin for everything you might need (or not know you need!). I could have merged the two IDE sections into one, but I do make a difference in my mind because of how JetBrains are advanced and ready-to-be-used. VS Code, whether you like it or not, is an important piece of software. Microsoft has made a big change with it, since most developers would agree it’s a good editor and environment to develop in, and it helped introducing things like LSP and DAP. You cannot trash-talk VS Code in a non-joking way, they contributed too much and we all use their work.
And then, you get into the “do one thing and do it right” way of working, but it’s more complicated than that. Historically, you would find tools such as Vim, Neovim, the git CLI, fzf, terminals, shells, TUI applications, etc. However, as time passes, there is a trend: people tend to transform those “unit tools” into IDEs. The terminology doesn’t really matter (whether you want to call that IDE or your own neologism). What matters is that such tools are not minimal nor “do one thing and do it right” anymore. Neovim, for instance, is now more a Lua interpreter in disguise of an editor, allowing to build via the plugin ecosystem, than a minimal editor. I was told that I was wrong thinking that Lua wasn’t part of the equation since the beginning, so yeah, I was a bit on the wrong track from the start.
It’s the same trend as it has been in the Vim ecosystem for so many years. Just look at plugins like vim-fugitive or nerdtree. In Neovim, you have plugins like mason.nvim (which is basically yet another package manager), lazy.nvim (same thing but different), nvim-tree.lua, a file explorer, neogit, a Git implementation in Lua, and the list goes on.
So, yes, I also contribute to that “plugin-based” ecosystem with hop.nvim
and mind.nvim, but I’ve been thinking about all that quite a lot. That adds
up to what I discuss in my article about configuration vs. scripting (basically, I think configuration should
remain data, not code — which what scripting is about). Quoting something I said on the Neovim Matrix room, “where
people see power, I see weakness”. A scripting language brings lots of powerful things, such as extensibility, but
it also brings bugs, hence unstability, and a Turing-complete language, preventing the host (i.e. Neovim or even
plugins) to easily use the configuration to discover options and data, without having to standardize an API. The
most infuriating point here to me is colorscheme. They are just scripts. Some of them are even stateful (they cache
things in ~/.cache/nvim). So “applying a colorscheme” is not simply just switching highlights mapping: it requires
to execute code, which makes it super hard to reason about the colorscheme. What’s a colorscheme when you know it
can run an HTTP command?
I’ve been seeing a lot of new plugins, since I am the author and maintainer of This Week in Neovim. And recently, I saw a video from TJ DeVries: Effective Neovim: Instant IDE. And that video confirms what I said above Neovim becoming an IDE. But it also made me realize something else; TJ uses a plugin as support for explaining how to easily turn your favorite editor into an IDE: kickstart.nvim.
That plugin is basically a single Lua init.lua script which serves as a starting init script for your configuration.
You just install it and it downloads a bunch of stuff for you, and calls all the required Lua code to set up correctly
the LSP implementation, Tree-sitter, downloading managers (yes, plural,
because you need one for plugins and one for LSPs, debuggers, linters, etc.), installing various plugins for
completion, Git integration, surrounding pairs, etc. And then, I wondered: “Newcomers are expected to run a
script that can download pretty much anything from the Internet… or write pretty much most of what it does — which is
a lot — on their own?!” And the thing is that, my own Lua configuration, which is not based on
kickstart.nvim since I created years ago, has been completely obsolete and I often need to go back to it to remove
or update things, especially regarding LSP, completion, snippets and all.
Most people from the community I talked to disagree with my point of view regarding Neovim. For them, plugins like
mason.nvim are amazing, because they close the gap between their
editor and the tools required by their editor to work correctly (Mason downloads LSPs / linters / DAP
adapters / etc.). I used Mason too, but eventually stopped using it when it started downloading a version of
rust-analyzer that was released years ago (that was a bug in Mason, I
guess?). I came to the conclusion that I was depending on something doing HTTP calls to download tools that, in
theory, could be used by other tools on my machines, and that I could also download myself very easily. In the case
of rust-analyzer:
rustup component add rust-analyzer
Worse… some of those tools are actually packaged in my package manager (pacman), so I’m basically using a tool
(Mason) that is doing the same thing as a package manager. As if we did not have enough package managers already.
I then continued thinking about all those plugins (among some I use or even have created, like Mind!). Why should I use them in Neovim? I’ve never been a file explorer guy that much, but I know about nvim-tree.lua and… and why do we have to have that in our editor? I remember the state of mind I was in when I wrote my article about Doom Emacs, which completely changed the way I think about software development. Emacs doesn’t belong to the “minimal editors”, nor the IDEs directly… it’s a different beast on its own, but if I had to compare it to something else, I wouldn’t say VS Code, or Neovim, or anything else. I would say “My terminal with all of the commands I run, including an editor, a git implementation, etc.” Wait a minute. Why are we trying to push all those features as plugins into Neovim, again?
The reason I didn’t stick around Emacs was basically because of its runtime choice, and ultimately, its Lisp ecosystem. The Emacs community is one of the best I have been talking to, and they have really, really talented people working on insanely complex topics, like turning Elisp code into native code (via C), including Emacs itself. But even with all those optimizations, the editor was still feeling too slow, and has a lot of background that you can feel. All the abstraction layers, all the Lisp macros (oh no), etc.
Eventually, I went back to thinking about that sentence… that haunting sentence… “Do one thing, and do it right.” That sentence has a lot of meaning and I think people have been tearing and bending it to align with their conviction, completely ignoring their biases. I read people from the Emacs community stating that yes, Emacs still applies to that sentence, because “It’s just a Lisp interpreter.” But in the end, the experience users have highly depends on the plugins, which is the same situation as with Neovim. And they have to configure their editor using a Turing-complete language that might introduce bugs, complex statements (who loves to set up a colorscheme by conditionally importing / requiring a Lua file located you-have-no-clue-where on your file system?)
Why are we trying to push all those features as plugins into Neovim, again? Why am I not trying to focus on using tools with a narrower scope, but ensure that the tool remains stable, powerful to use and compose well with the rest?
Lately, I have discovered Helix, a “post-modern modal editor”. The first thing I
noticed was that it’s different than Neovim in terms of motions. In Neovim, in normal mode, you start with a
verb, like deleting is d, and then you type a motion, like a word is w. So typing dw on your keyboard will
delete a word. In Helix, it’s reversed. You first select with w, and then you apply the verb you want.
So wd. At first, I thought is was a neglible difference. Then I realized how more powerful the [Helix]’ way ways.
Since you “see” the selections, you can select first and then decide what to do, or even change your mind and extend
a bit the selection. You have this nice visual feedback.
And then comes all the good stuff. Helix comes with those included features, requiring zero configuration:
And it has no plugin support for now (they plan on adding it at some point, but not for now). And that made me realize:
my editing experience in that editor — even though it took a couple of days to adjust the muscle memory — has been
flawless. So yes, I miss my hop.nvim plugin, but I realized I could hack around by using
Kitty hints until that kind of motions is built-in.
However, I’m just talking about editing, here. I still have that reflex of pressing SPC g g to bring up a Git prompt
in my editor to start committing… but Helix doesn’t have one. So I’m splitting my terminal into another window and
I use the git CLI. And it’s fine. Now, the way I think about it is that I could probably invest time into learning
lazygit or anything else.
The point is that my editor is now minimal again. My configuration (which is public, you can find it here) is mostly about bépo remappings and some very minimal aesthetics changes. The configuration is data (it’s just a TOML file), and I don’t have to worry about stability anymore since I’m not using any plugin. The fact that I have an amazing editing experience (even better, honestly, due to the selection then verb principle, multi-cursors, out-of-the-box LSP and Treesitter experience, including completion) is just the perfect fit for what I want.
But there’s a catch. See, Helix is about editing. If you like to have a file explorer in your editor, the way I would recommend looking at Helix is that you cannot have it in Helix and you should probably use a proper, standalone file explorer, or consider another alternative like Neovim. If there is something you would like to add to Helix, you have to open a PR and write some Rust code. You cannot extend it on your own, as it doesn’t have plugin.
To me, that’s great, because it means the scope is under the responsibility of the Helix Team. And I love that. I love it because it’s easy to think about the features of my editor. It’s easy because I don’t have to keep fearing something break because of an incompatibility between a plugin and the version of the editor I’m running (or even two plugins between each other).
And honestly, contributing using a statically and strongly typed language (Rust) feels so much sounder to me than using something like Lua. You can benefit from all the tests of the code base, use the API without any ABI conversion in between, and catch bugs at compile time instead of waiting for them to crawl up at runtime.
My view on that hasn’t changed since my last articles. I still think that the terminal needs to be revamped and go into a direction similar to Emacs. I have started a project a couple of months ago that tries to explore that. Basically, I’m making something that is not a terminal, a shell nor editor. It’s “a platform”, with primitives like tree views, item lists, read-only / read-write buffers, virtual text, popups, command outputs, cells, etc. And it comes with no way to edit text or file explorers, or anything.
Then, applications targetting that platform can use all those primitives to compose and now the features are emergent. A text editor then uses the buffer, virtual text and popup primitives, for instance. A file explorer would use the tree view primitive. An LSP client could be a daemon that attaches to edit buffers. Etc. etc.
That’s the dream tool I would love to see, and I still think that Emacs is the closest thing to that, but it comes with too much legacy to me. And it’s unlikely that my experiment will ever be mature enough to be usable or even used. But you know, I like experimenting. A cool project to play with is nushell. It’s far from being that dream platform of mine, but it has some very interesting ideas for composing commands that I think are worth mentioning.
And no, I don’t want Helix to become such a platform. Nor Emacs to get rid of its legacy and become it. Nor
Neovim. I want to keep playing with Helix and using it for what it is (and shines for!): editing code. If my
dream platform ever exists, whether it’s mine or someone else’s, I will probably move away from Helix to whatever
that platform provides. But such a change would require a standardization, such as stdout and stdin, but with all
those primitives I mentioned. And I’m not sure whether such a thing will or can exist.
I’m not going to give my complete opinion on Helix just yet. I have been using it for a couple of days only, and at the time writing, it still has a lot of missing parts / experimental ones. For instance, its DAP support is experimental, so I can’t judge. What I plan to do is to stick to it and move away from “making my own IDE in an editor” and instead enjoying composing tools on the CLI. Then, when I have enough hindsight, I will give a fair review of Helix.
mind.nvim, hop.nvim and This Week in Neovim?So, about mind.nvim, I plan on rewriting the plugin as a standalone tool so that I can use it whatever the editor.
It will probably do things like git when you run git commit (opening $EDITOR), but I’m still not sure exactly
how I’m going to make it. Maybe I’ll get in touch with people from Charm and rewrite it using some
of their work? I still haven’t thought about it, it’s too early.
About hop.nvim, I plan on continuing maintaining it and fixing bugs, even though I haven’t been very active around Hop
lately. The reason is mainly a lack of spare-time.
As for This Week in Neovim… I honestly do not know. I discussed the project with some people from the Neovim core team, and I’m a bit stuck. On one side, the community has received it pretty well, given the amount of upvotes I have on each week release on Reddit; the comments; the appreciation issues on GitHub, etc. I know people have been enjoying my work, and I’m happy they do.
On the other side, the core team doesn’t seem to have noticed it that much, and none of their members approached me to talk about it. So I’m not sure what to think. The community enjoys TWiN a lot; the core team doesn’t really care. Then I need to think about exhaustion: I’m really tired of maintaining TWiN.
See, the idea is to communicate, every week, about what has happened in the Neovim world, whether it’s core or plugins. What I had initially in mind was to bootstrap the couple of first releases and let people adopt and contribute to it. On the 2nd of January 2023 is released TWiN #25, which means that I’m currently on a 25 weeks streak. What it basically tells is that, every week (most of the time Sundays), I skim Reddit, GitHub projects, man pages, etc. to get as much information as I can, and create a really big PR containing the weekly update. That PR is merged and available on the very next day (Monday) for every neovimmers to enjoy reading on Monday morning with a nice cup of coffee, tea or whatever you like for breakfast.
So every week, one person (me) spends hours skimming many projects, while what I thought would happen was that many plugin authors would contribute once every two months a very small text to explain their new plugins / change. The difference is massive: on one side, you have a single developer doing a big amount of work… every week. On the other side, you would have many developers doing a very small amount of work every time they release something… which is clearly not every week (and even then?).
I think I have enough distance with the project to admit I failed marketing my idea. Someone once told me that I was basically doing free advertisement for plugin authors, which is actually true. People mention they would like to donate to contribute and ensure that I keep doing what I do, but I don’t want money — hosting costs me 10€/month and the domain name is 10€/year, I can sustain that on my own. What I need is contributions. It wouldn’t cost much for a plugin author to open a PR to twin-contents and add their update to the upcoming week. There’s a few regular contributors, writing good PRs I rarely need to modify. But most of the weeks are contribution-free, and it saddens me even more when I see the reaction of plugin authors on Reddit, like “Oh yeah my plugin made it to TWiN!” Every time I read that, I think “Great, next time maybe they will be pushing the update themselves to help me?” And most of the time, they don’t.
So, people started to mention that I should slow down or I will burn out. And I’m honestly pretty fed up with this read-only relationship: people consume / read; they rarely contribute, even when they could contribute their own update for their own plugins! I don’t filter out anything, as long as it’s about Neovim and doesn’t convey any bad speech (you know the deal). TWiN is about new plugins, updates of existing plugins, tips of the week, blog articles, youtube videos, etc. Anything Neovim related produced by a member of the community. And even with the exposure of TWiN, people still do not contribute. Even after the big refactoring to ease contribution I announced on Reddit. So yes, it’s a personal failure to market my idea regarding TWiN, and I’m not sure what the next steps are.
Nevertheless, we all learn from mistakes and it’s important to understand them. I will collect my thougths and decide
what to do next. For the time being, I wish you all a Happy New Year, lots of great things, and a tremedous amount of
happy hacking, in your favorite editor, IDE or whether you pipe echo commands directly at the end of files!
Keep the vibes!
]]>
comptime is probably not as interesting as it looksAh, Zig. I have a love-hate relationship with this one. A “new” (reading: appeared a couple years ago, already — yes, already), language with high ambitions. Zig was made to run at low-level, with a simple design to solve many problems C has (macros, allocators, error handling, more powerful types like baked-in tagged unions and bitsets, a better build system, no hidden control flow, etc.). The language claims to be the C successor, and today, many people claim that Zig is simpler and even safer than most languages out there — even Rust! — allowing to focus more on the technical challenges around your problem space rather than — quoting from the Zig mantra — your language knowledge. I think I need to put the full mantra because I will reuse it through this article:
Focus on debugging your application rather than debugging your programming language knowledge.
We will come back to that.
I had already written about Zig a while ago when I initially approached it. I thought the language was really interesting and I needed to dig deeper. That blog article was made in July, 2024. I’m writing these lines in February, 2025. Time has passed, and yet I have been busy rewriting some Rust code of mine in Zig, and trying out new stuff not really easy or doable in Rust, in Zig, just to see the kind of power I have.
Today, I want to provide a more matured opinion of Zig. I need to make the obvious disclaimer that because I mainly work in Rust — both spare-time and work — I have a bias here (and I have a long past of Haskell projects too). Also, take notice that Zig is still in its pre-1.0 era (but heck, people still mention that Bun, Tigerbeetle, Ghostty are all written in Zig, even though it hasn’t reached 1.0).
I split this article in two simple sections:
Zig has many interesting properties. The first one that comes to mind is its arbitrary-sized integers. That
sounds weird at first, but yes, you can have the regular u8, u16, u32 etc., but also u3. At first it
might sound like dark magic, but it makes sense with a good example that is actually a defect in Rust to me.
Consider the following code:
struct Flags {
bool clear_screen;
bool reset_input;
bool exit;
};
// …
if (flags.clear_screen || flags.reset_input) {
// …
}
That is some very typical need: you want a set of flags (booleans) and depending on their state, you want to
perform some actions. Usually — at least in C, but really everyone should do it this way — we don’t represent
such flags as structs of booleans, because booleans are — most of the time — 8-bit integers. What it means is
that sizeof(Flags) here is 3 bytes (24 bits, 8 * 3). For 3 bits of information. So what we do instead is
to use a single byte and perform some bitwise operations to extract the bits:
#define FLAGS_CLEAR_SCREEN 0b001
#define FLAGS_RESET_INPUT 0b010
#define FLAGS_EXIT 0b100
struct Flags {
uint8_t bits;
};
bool Flags_contains(Flags const* flags, uint8_t bit) {
return flags.bits & bit != 0;
}
Flags Flags_set(Flags flags, uint8_t bit) {
flags.bits |= bit;
return flags;
}
Flags Flags_unset(Flags flags, uint8_t bit) {
flags.bits &= ~bit;
return flags;
}
That is obviously very error-prone: we use CPP macros (yikes), bits are not properly typed, etc. Zig can use its arbitrary-sized integer types and packed structs to automatically implement similar code:
const Flags = packed struct {
clear_screen: bool,
reset_input: bool,
exit: bool,
};
This structure has two sizes: its bit-size, and its byte-size. The bit-size represents the minimum number of bits it uses (3), and the byte-size represents the number of bytes required to hold the type (1). We can then use it like so:
if (flags.clear_screen or flags.reset_input) {
// …
}
This is an awesome feature, especially because lots of C libraries expect such bitfields, for instance
in the form of a u32. You can easily and naturally convert the Flags type to a u32 with
@bitCast(flags) — you need to ensure the booleans are in the right order (big endianness here in Zig
if I recall correctly).
Note: in Rust, we don’t really have a nice way to do this without requiring a dependency on bitflags, which still requires you to provide the binary value of each logical boolean in binary, usually done with
constexpressions using1 << n..
As a Haskeller, this is also something that makes a lot of sense to me. A typical struct Vec<i32> in
most languages is actually a function taking a type and returning a type in Zig;
fn Vec(comptime T: type) type.
Although more verbose, it allows a lot of flexbility, without introducing a new layer specific to the type system. For instance, specialization can be written in the most natural way:
fn Vec(comptime T: type) type {
if (@sizeOf(T) == 0) {
return VecAsUsize;
} else {
return struct {
// …
};
}
Another use case that I think is pretty nice is when you need to implement something that depends on the actual type structure. Zig has compile-time reflection, which means that you can analyze the fields, type information etc. of a type to implement a specialized version of your algorithm. You can then write your JSON serializer without depending on a middleware (e.g. in Rust, serde).
This one is a half love / half dislike — you will find the dislike part in the appropriate section of this article.
In Zig, the core of error handling is Error Union Types. It’s straight-forward: take an enum (integer tags)
and glue it with a regular T value in a tagged union. You either get the error discriminant, or your
T value. In Rust terms:
enum ErrorUnion<T> {
Err(ErrorType),
Ok(T),
}
There’s a catch, though. Unlike Rust, ErrorType is global to your whole program, and is structurally typed. Error
types are declared with the error {} construct:
const MyError = error {
FileNotFound,
NoPermision,
};
Error types can be glued together to create more complex error types:
const OtherError = error {
OOM,
NotANumber,
};
const AppError = MyError || OtherError;
Thus, an Error Union Type is either an error or a value, and it’s written E!T (E the error type, T
the value). An interesting aspect of that is that all error types are flat (there is no nesting), and
because they are nominal, you can even return error values without declaring them in the first place. If
you do not care about the actual type of your error, you can use the anyerror special type to refer to
the global error type, or leave it empty (!T) to infer the type based on the body of the function.
All of that is interesting, but one very cool aspect that I think I really miss when writing Rust is
coercion. Because of coercion rules, a regular value T coerces to E!T, and an error type E coerces to
E!T. So you can completely write this:
fn foo() !i32 {
return 3;
}
And the same is true for void:
fn checkOrError(input: i32) !void {
if (input < 10) {
return error.TooSmall;
}
}
There is no need to wrap results in “success paths”, such as Ok(()) in Rust.
If you need to work with C libraries a lot, Zig has some really good features and built-ins baked-in,
especially if you combine them with comptime functions to perform various transformations automatically.
@cImport / @cInclude allow to read a .h, parse it at compile-time, and expose its content as Zig
symbols (functions, constants, etc.), exposed in a big struct. For instance:
const c = @cImport({
@cInclude("GLFW/glfw3.h");
});
// c.glfwInit() is now available
This is honestly pretty nice, especially since you can iterate on the contents of c with an inline for
at comptime to transform functions the way you want.
The build configuration of your project is written in Zig. Even though I don’t think I like configuration as code, it’s still an interesting idea. It will probably feel like fresh air for people having to use CMake or similar tools. Zig build module is not very complex to understand and allows a great deal of flexibility when configuring your targets, optimizations, CPU architectures, ABI, CLI options, steps and all.
At the current time of writing this, however, even zig does build and package, dependency handling is far
behind anything you might be used to (cargo, pip, node, cabal, etc.). I don’t think it would be fair
to judge it for now.
Even tough Zig is full of nice surprises, it’s also full of what I would call flaws that — personal opinion – make it a bad fit for robust and sound systems.
As mentioned earlier, error handling in Zig is nice, but it lacks one important feature: you can’t carry values. It’s likely due to the fact errors flow via a different path than “normal” code, and require owned values, so in many cases it will require allocations, which is not something Zig wants on its error path.
It makes sense, but it’s really annoying. Something like error.FileNotFound will require you extra
code infrastructure to find exactly which file was not found — maybe you can deduce it from the caller
and match on the error via catch — but maybe you can’t (the path might be computed by the function
returning the error). You can’t even pass integers around in errors, since it’s already used for the
variant of the error itself.
Coming from Rust, obviously, that feels very weak. The Result<A, E> type — that I’ve been using in Haskell
as Either e a — is a godsend. Not having something like that in Zig creates frustration and will likely
generate less interesting error values, or more convoluted code infrastructure around the callers.
On a similar note, the try keyword (which takes an E!A expression and is equal to A or return E;,
depending on the presence of error) allows to propagate errors, but it won’t do much more than that. You can
think of try foo() as the same as foo() catch |err| return err;. That obviously works only with
error union types, so if you happen to have a function returning a ?A (optional), you can’t shortcircuit
in a nice way with try and instead needs to use the more verbose orelse return null;. This monadic
approach is pretty powerful in Haskell and Rust, and it wouldn’t hurt much to allow it for error union
types and optionals, since both are built-ins (so you don’t get to pay for the complexity of it).
Another thing I dislike about try is that it’s a prefix keyword which chains really badly:
try (try foo()).bar()
Shadowing is the act of using the same name for two different bindings available in a scope hierarchy. Rust is an excellent example of how to do shadowing correctly. For instance, in Rust:
fn foo(input: &str) {
// input refers to the argument and has type &str
let input = input.to_owned();
// input refers to this function stackframe local owned string and has type String
// we don’t have access to the argument anymore
{
let input = 12;
// input is integer, and we don’t have access to the String in this block
}
// input is the String
}
This lexical scoping in Rust prevents tons of boilerplate or having to come up with useless names. Zig not having that causes annoyances, especially with error handling:
const foo = Foo.init();
const foo2 = try foo.addFeatureA();
const foo3 = try foo2.addFeatureB();
const foo4 = try foo3.addFeatureC();
// etc.
This is maybe a sign of the design choice (no move semantics), but all in all, I don’t like having to come up with either obscure names or just bad names for something that should be chained calls, or just reusing the same name. Sometimes, naming things is something we should refrain from.
This is an important issue I have with Zig, as it’s present everywhere and implies a lot of cascaded issues.
comptime is not only a keyword, as you might have guessed. Take this example for instance:
fn doMath(x: anytype) @TypeOf(x) {
// …
}
There is no way to know what that function requires as input. The very first line of the zig zen states
* Communicate intent precisely.
And even though I do agree with that, I think it’s poorly implemented, for two reasons:
The second point is even violated by the standard library itself. Consider std.fs.Dir.iterate, which is a function used to iterate on a directory entries. There is no documentation at all, nor even comment, and you are left with its implementation, which is:
pub fn iterate(self: Dir) Iterator {
return self.iterateImpl(true);
}
You can click on Iterator to end up on another
documentation page with no actual documentation and a lengthy implementation. I highly suggest reading the comment
on the next() function:
/// Memory such as file names referenced in this returned entry becomes invalid
/// with subsequent calls to `next`, as well as when this `Dir` is deinitialized.
This is incredibly error-prone, and the mere fact that even the standard library fails to abide by the very first
rule of zig zen proves the point that communicating intent precisely via documentation is hard, and probably not a
good design decision for a modern language.
And there is worse. As your software matures and you write more and more code, you will eventually change some internals. Imagine what would happen if the documentation and/or comments go out of sync with the code. In languages with type systems, it’s a minor annoyance — you will get angry people complaining about the fact the documentation has typos / errors, and fixing them will be a patch release. In a language such as Zig where the documentation is the contract, the source of the issue is vague and harder to understand: do they misuse the function? Is the function buggy? Both?
Another point that I want to discuss is the lack of traits. Zig is a simple language and per-se, I guess, traits were ruled out — and don’t even think about proposing them. However, they do solve a real-world problem: programming contract. There are two important parts:
Programming contracts convey useful information to the programmers. If you have a function like the doMath
presented above, you have no clue what you are supposed to call it with. If you have something like:
fn do_math<T>(input: T) T where T: Add + Mul {
// …
}
You know exactly what you can call it with. Additionally, you get discoverability — it’s super easy to build a reverse list of implementations based off some traits — Rust does it for you in both the docs and LSP.
Programming contracts are part of the public-facing side of your API. They should be items that are versioned as the rest. If you decide to change the allowed properties of an input, just updating the documentation will make it harder to realize you have to do a minor bump and not a patch bump.
Moreover, typeclasses allow for more control on the code you don’t run / test. If someone decides to implement your trait for their types, having a clear interface to implement is far sounder than having to go fishing for the programming contract in the documentation, assuming it even exists in the first place.
comptime is probably not as interesting as it looksI’ve been wondering a lot about this one lately. Yes, compile-time reflection is nice… but is it really?
When you think about it, implementing a JSON serializer for anytype is probably very unlikely to be
completely true. There will be hidden constraints. For instance, does it make sense to serialize a
pointer? Probably not, so the implementation might check the type info and refuse to serialize a
pointer. And — c.f. the previous section — that information is missing from the interface.
When you think about it, this problem is everywhere, as soon as you use comptime, because of the lack
of traits. If you want to understand the contracts / preconditions / postconditions / invariants… just
read the documentation… and the code, because well, you can’t be sure that the doc is not out of sync.
comptime is one of those features that feel like a candy, but has a bitter taste to it. It feels like
C++ templates back again, and the community will not address that.
I hate the idea. After decades of programming, I can say with full certainty that besides teams with extremely talented people that have been doing that kind of things for more than I’ve lived (old C programmers, Linux maintainers, maybe a bunch of driver maintainers), it will only end up with tears. I can’t in good faith suggest to use Zig at work given how complex our programming contracts are. I’m far from being a perfect programmer — I make mistakes. We do have junior developers in our teams, work or spare-time projects, people more or less used to our code bases and guidelines, and we are not 100% perfect while reviewing either. Sometimes some code will be reviewed by someone else that is less severe than you are. Sometimes it’s your future-self that will actually misuse something you thought was easy to understand. Etc.
This is a continuation of the previous section. For a reason that I don’t understand, Zig doesn’t have
encapsulation. What it means is that if you use a type such as ArrayList(i32), you must know how it’s
implemented to be able to get useful functions and properties, such as its len length. Still with the
same example, the documentation of ArrayList
doesn’t show anything regarding the length of the array, nor even a way to iterate on its
elements. You have to know — read the code please — that what you are looking for is .items.len for the
length. If you want to use the for loop syntax, you do it on the .items:
for (array.items) |item| {
// …
}
Even worse, you can actually mutate the internals. How do we ensure that we can expose types to our users
without causing them to break invariants? Well, zig zen: communicate intent precisely, of course! Redirect
them to the code and ensure to slap // INVARIANT in your comments / documentation.
Sigh…
I’m a bit dishonest, though. Zig does have a form of access control. At the module level, you can decide
whether a symbol is private (default), or pub. That’s all you get.
In essence, this is similar to C.
ERRATUM (Fri Feb 7 2025): criticism was made regarding this section for being unfair, and after several days, I think I do agree with it. I initially mentioned miri because it’s easy to get it via
rustupand feels baked in, but a honest comparison would have be to compare it to Zig + Valgrind (or similar).
I read in numerous places that Zig is safer than unsafe Rust, which is not true, and fallacious.
Andrew Kelley wrote a blog article
where he built his argumentation on the fact that Rust will compile and will run into UB, while Zig won’t.
Here’s the Rust code:
struct Foo {
a: i32,
b: i32,
}
fn main() {
unsafe {
let mut array: [u8; 1024] = [1; 1024];
let foo = std::mem::transmute::<&mut u8, &mut Foo>(&mut array[0]);
foo.a += 1;
}
}
The first thing to say is that there is indeed a UB, and it’s not easy to spot; transmuting a &mut u8 to
&mut Foo is the same as casting an aligned pointer to an unaligned pointer, which is indeed UB. Back when
he wrote his article, I’m not sure he knew about miri — which has been around since
October 2017 — given that his article was released in 2018,
but even then, I think it’s important to draw people’s attention to something important here. Zig does have
this unaligned pointer cast detection by default, but you’re just one rustup components add miri call away:
cargo miri run
…
error: Undefined Behavior: constructing invalid value: encountered an unaligned reference (required 4 byte alignment but found 1)
--> src/main.rs:9:19
|
9 | let foo = std::mem::transmute::<&mut u8, &mut Foo>(&mut array[0]);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ constructing invalid value: encountered an unaligned reference (required 4 byte alignment but found 1)
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
= note: BACKTRACE:
= note: inside `main` at src/main.rs:9:19: 9:74
note: some details are omitted, run with `miriFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error; 1 warning emitted
It would have been nice to point that out. Yes, Zig has it by default, but Rust has it too by adding a tool
that is available via rustup. I do have to agree that unsafe Rust is probably harder to write than
regular Zig, because of all the rules you have to manually ensure not to violate (stacked borrows, etc.), and
for 90% of the time, what you will be doing will be to call a C function via the FFI, so you should be fine.
For more complicated unsafe usage, just use miri. Actually, I think it should be a good practice to use
miri, at least in the CI.
I could understand Andrew missed that when he wrote his article (hm…). But today? People still mention that Zig is sooooo much safer than Rust.
So let’s see how Zig is safer now. Up to this day, Zig doesn’t see Use After Free (UAF). Not at compile-time. Not at runtime. They are just plain Undefined Behavior (UB). For instance:
const std = @import("std");
fn getPtr(value: i32) *i32 {
var x = value;
x += 1;
return &x;
}
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const ptr = getPtr(123);
try stdout.print("ptr.* = {d}\n", .{ptr.*});
ptr.* += 10;
try stdout.print("ptr.* = {d}\n", .{ptr.*});
}
If you compile this with the default optimization option (Debug), you get this:
$ zig build run
ptr.* = 124
ptr.* = 134
If you compile with ReleaseFast:
$ zig build -Doptimize=ReleaseFast run
ptr.* = 0
ptr.* = 0
Oh le malaise. This, here; this exact situation is the main reason why I refrain myself from using Zig
in production. The idea that I could end up in such a situation and not even knowing about it. There
are discussions to track all possible UB and turn them into checked illegal behavior.
That issue has been opened since 2019. I’m not saying it won’t ever be a thing — I guess it’s required
to hit 1.0. But still. The language cannot in good faith state that it’s safer than unsafe Rust. Let’s
convert that code to Rust and run miri on it…
I would obviously never suggest to use raw pointers but instead references, but references will trigger a compilation error so it’s not really fair for this comparison, and the Zig community always compares it to
unsafeRust, so let’s get unsafe!
fn get_ptr(input: i32) -> *mut i32 {
let mut value = input;
value += 1;
&mut value as *mut _
}
fn main() {
unsafe {
let ptr = get_ptr(123);
println!("*ptr = {}", *ptr);
*ptr += 10;
println!("*ptr = {}", *ptr);
}
}
Running miri:
error: Undefined Behavior: out-of-bounds pointer use: alloc565 has been freed, so this pointer is dangling
--> src/main.rs:10:31
|
10 | println!("*ptr = {}", *ptr);
| ^^^^ out-of-bounds pointer use: alloc565 has been freed, so this pointer is dangling
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
help: alloc565 was allocated here:
--> src/main.rs:2:9
|
2 | let mut value = input;
| ^^^^^^^^^
help: alloc565 was deallocated here:
--> src/main.rs:5:1
|
5 | }
| ^
= note: BACKTRACE (of the first span):
= note: inside `main` at src/main.rs:10:31: 10:35
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
The message has some weird mentions in (alloc565), but the actual useful information is there: a pointer is
dangling.
So no, I strongly disagree that Zig is safer than — even — unsafe Rust. Anyone telling you otherwise is
either purposefully lying, or ignorant. And I think I need to mention the misconception that you need to
drop to unsafe often in Rust. This is simply not true. Some libraries — especially interfacing with C
libraries — do use unsafe to make FFI calls (usually, they just do that). unsafe might be required for
some very specific tricks required to implement safer abstractions, but you are not supposed to write a full
library or application in unsafe.
Just test your software correctly!
Again that argument… UB cannot be tested and requires statistical analysis — or some kind of runtime protections that is currently not implemented in Zig — and coverage in a langage that is built off lazy compilation everywhere is probably not something I will discuss here…
Heck, I thought I would dodge this one. So… yeah… this is a bit embarrassing, but Zig implements lazy compilation.
The idea is that you write code, and it doesn’t compile it. That sounds so exotic that the standard library
has a helper function to mitigate that weird design decision (std.testing.refAllDecls).
You must use it in a very natural way:
test {
std.testing.refAllDecls(some_module_of_yours);
}
It’s a common idiom I have seen in many places (Ghostty 1 2 3 4; TigerBeetle 1 2; vulkan-zig hmm).
So… if everyone really mitigates this “feature”… was it really worth it? In the end, it makes sense not to include code that is not participating in the final binary, but… have you thought about refactoring? Have you thought about systems that add code that will be used later? That happens a lot and I was bitten so many times while writing some disjoint sections of code and having to spend much more time later when gluing everything together — because the code was actually never checked!
And there is the opposite problem. Zig makes some assumptions on what is important, so obviously, a parameter of a function that is not used should be a hard error. It lazy-checks functions you wrote and ignores them if you don’t use them right away, but refuses to compile arguments you ignore?! I mean, I get why (not using an argument could hide a bug), but a warning would suffice.
I haven’t found a way to disable that linter and make it a warning, and I think it’s unlikely to ever happen.
This one I could get, but not implemented the way it is. Zig doesn’t have any sound way to ensure proper resource management — just like C. See the previous section about communicating intent properly. Zig requires the call site to deallocate properly, and you have to mention the deinit logic in your documentation.
defer – and errdefer, which is for a different usecase, but it’s not really important here — is a tool you
can use at call site to implement resource cleanup, whether it’s memory deallocation or file descriptors close.
The concept has been around for a long time, and as mentioned in the previous sentence, it’s not automatic. The
caller must know that they have to call defer. The documentation might forget to mention it and the user
might forget to call it. On memory cleanup, if you are lucky and your tests run that code, you will get a traced
leak. For more complex resources such as GPU queues, database handles, etc., well, it’s probably a leaked file
descriptor?
I’m not entirely sure whether destructors are the best solution to this problem, but they allow to ensure that
the code calls the cleanup routine. There are alternatives — explored in ATS, probably too complex
for now, requiring linear types and/or proofs to force the caller to get rid of the resource — Rust could have
something along those lines, since it has move semantics and an affine type system; I don’t think people will
trade Drop for linear types though.
It’s a bit a pity, to be honest, to see such a design in Zig, because it does have the infrastructure to do better in my humble opinion. For instance, this won’t compile:
fn foo() i32 { return 123; }
// …
pub fn main() !void {
foo();
}
Because the return value of foo is ignored. To make that compile, you need to explicitly ignore the
returned value:
pub fn main() !void {
_ = foo();
}
So it’s a bit weird to see that Zig can prevent compilation if you do not use an argument or the returned
value of a function, but doesn’t provide the proper tooling to force the user to correctly deinit
resources. If you pull the problem a bit more, it shows that the design of Zig — in its current state —
doesn’t permit that, since you would need linear types/values (i.e. a value must be used once and only
once). I would have loved something like a # linear marker that would cause a linear resource to be dangling
if it’s not eventually consumed by a function taking it with the marker as argument:
const Resource = struct {
…
pub fn init() #Resource {
…
}
pub fn deinit(r: #Resource) {
}
};
Obviously, as soon as you see that, it causes issues with the Zig type system, because you can still bit-copy values, so you duplicate a resource and might cause double-frees — but copying should be disallowed in a linear type system. So Zig cannot have linear types/values without changing all the properties of such types, and purely linear values are often not enough; we eventually want to weaken the restriction (relevant type system) to be able to use the resource value in a controlled way, via, for instance, borrowing. Which leads to more complexity in the language, so clearly not something we will ever see in Zig.
The standard library doesn’t have a good support for unicode strings. There is a discussion opened by
Drew DeVault about improving strings and unicode handling.
I share the link so that you make your own opinion about what to think of all that, but not having a
string type and recommending users to “iterate on bytes” is a big no to me. It’s the open door to a wide variety
of issues / corrupted data. People in the thread even recommend using .len on []u8 to get length of strings
(never do that, unless you are doing Advent of Code).
It will never happen, whatever your arguments. You are left with user-land support, such as zg.

I get it. Zig has the ambition to replace C, and as such, doesn’t want to have to deal with complex abstractions. It trades memory safety and soundness for simplicity. If I’ve been around Zig and actually writing Zig code that much, it’s because I wanted to check whether memory safety is a red herring myself. In the end, maybe Zig is right. Maybe we can achieve enough with a simple language, and deal with the remaining issues with other approaches. There is nothing wrong with that.
However, I think it’s too soon to make any useful conclusion. I really want to have a look at reports about projects that are entirely written in Zig (Ghostty, TigerBeetle, etc.) after they have matured enough. It’s great that those projects are successful with Zig; I’m honestly happy for them. But a robust and scientific approach requires us to go further than just assumptions and feelings. I do think we have data about CVE issues (we all know the 70% number, right?), and it took time and lots of software history to have enough hindsight. I do think that hindsight is required on Zig to know whether it’s actually contributing to more reliable software. Very personal opinion: given all the points I mentioned, I really doubt it.
I don’t think that simplicity is a good vector of reliable software. At most, it’s a happy side-effect. It’s not a requirement, and should remain a secondary mission. What the industry needs is to identify problems (we have) and designs solutions that solve those problems. Anyone has the right to select a subset of those problems (even Rust can’t solve everything) and solve those specifically, ignoring the others or pushing their resolution to user-land / process etc. But here, I think there is a big misconception.
Zig does enhance on C, there is no doubt. I would rather write Zig than C. The design is better, more modern, and the language is safer. But why stop half way? Why fix some problems and ignore the most damaging ones?
Remember the introduction:
Focus on debugging your application rather than debugging your programming language knowledge.
Do you see why this is such a poignant take? Is it really better to spend time in gdb / whatever debugger
you are using, or having your users opening bug issues than having to spend a bit more time reading compiler
error messages? That mantra seems to make it like it’s a bad thing to have a compiler yell at you because you
are very likely doing something you don’t really intend to. Why? It’s a well known fact that bugs don’t
write themselves. At some point, a developer wrote some code expressing a solution that was, in fact,
actually written a different way, in dissonance with the initial intent. This is part of being a human. So
why would you complain that a compiler tells you that what you are doing is very likely wrong? Because in a
few instances, it was unable to see that what you were doing was, actually, fine? You base a full language
experience based solely on some exceptional occurrences / issues? And trust me, I also do have those moments
with Rust from time to time (especially with the lack of view types and partial mutable borrows).
Seeking simple is just not the right move to me. To me, a more robust approach is ensuring people can’t shoot themselves, while seeking simpler. Simpler doesn’t mean simple; it means that you design a solution, whatever the complexity, and tries to make it as simple as possible. Rust is honestly not that hard, but it is definitely not a simple language. However, for all the problems it solves at compile-time, it’s definitely simpler than all other approaches (e.g. ATS). And it’s not unlikely that it will get simpler and simpler as we discover new ways of expressing language constructs.
I think my adventure with Zig stops here. I have had too frustration regarding correctness and reliability concerns. Where people see simplicity as a building block for a more approachable and safe-enough language, I see simplicity as an excuse not to tackle hard and damaging problems and causing unacceptable tradeoffs. I’m also fed up of the skill issue culture. If Zig requires programmers to be flawless, well, I’m probably not a good fit for the role.
]]>The goal of this article is to create a temporal “snapshot” of my views on editors and what I think about the current situation. I have been using vim and especially neovim for more than a decade. I need to explain about my current workflow and what I cherish in editing before talking about each editors. Expect a point of view from a neovimer which is looking around at lots of editors.
This is only a personal workflow / point of view that works well for me right now. It doesn’t mean it will for you and it doesn’t mean a different workflow would be worse.
I am French and I’m using a keyboard layout that is made to type very quickly in French and to code. With hindsight, since I type more often in English than in French, maybe I should have picked another keyboard layout, but the coding experience in my keyboard layout is really great, so I stick around.
The keyboard layout is bépo. I learned bépo the “recommended” way — i.e. you have to practice typing (« dactylographie » in French). It means that I use all my fingers to type on a keyboard, and that each key on the keyboard is assigned a single finger that will press it. That helps a lot with muscle memory and to reduce wrist pain (my wrists barely move when I type on a keyboard), among other things. The typing speed is a side-effect of being accurate and having good comfort (if you are curious, I’m pretty fast but there are faster people — I type at around 120 to 130 words per minute). Because I think the speed doesn’t matter when programming, I think the most important part to remember here is the comfort: the wrists don’t move and my fingers fly around the keyboard, whatever the speed.
I think a modal editor is superior, for various reasons. The first one is that I truly hate having to use a
mouse for something that can be done without having to move around hundred of pixels with your cursor and clicking
on buttons. For instance, running an application, on my current machines, is simply typing alt d, the name of the
program (I typically use completion, so I never type fully the name of the program) and hit enter. All this without
moving my hands from the keyboard. And I do that for programs like firefox, kdenlive, etc. but for terminal
applications, I simply type and complete them in my terminal, which I open simply with alt return.
So, using a mouse to move around a cursor in an editor feels like completely suboptimal to me, especially because we write code (i.e. we type on a keyboard) most of the time, so moving around with the mouse implies several back and forth movements between the keyboard and the mouse. Maybe you don’t care and it’s cool to you, but to me, this is truly horror land. I feel very uncomfortable when doing this.
Also, modern editors that are not modal typically make people move by using the arrows keys, which are either far on your keyboard, or — like my keyboard, a 60% home made one — not in direct access and then require a function key to enable them.
So that’s the first reason why I like modal editors. They make a smarter use of your keyboard for simple yet
recurrent features, like moving around — e.g. h j k l. The second reason why I like them is because of the facts
they have a completely new mode for non-modal editor (i.e. the normal mode), you have a whole keyboard / lots of
keys to bind actions to and do lots of things people usually do with the mouse. Being able to split an editor into
several buffers, move around the buffers, go at the beginning of the paragraph, search and replace, register actions
into macros and replay them, etc. All this without even moving the wrists. The learning curve is steep if you’re used
to the mouse, but once you’ve passed the mental barrier, really, and this is a personal opinion, but I truly think
that using the mouse again after that feels like handicap to me.
When I look at people coding, I see several kind of programmers:
h j k l in modal editors. You can spot them very easily at how the
cursor moves in a document. It typically implies keeping a key pressed until the cursor reach a given row, then
pressing another key until the cursor reach a given column and then adjust if they went passed the location they
had in mind.$ (go to end of line),
f (go to the first occurrence of the next character typed after f, like f( will make your cursor go to the
next (), % (go to the matching delimiter) or w (go to the beginning of the next word) / b (go to the
beginning of the previous word), etc. to move faster on the same line (it works across lines too).I think that represents 99,9% of what I see people do. Obviously, you will get that I don’t belong to the second set
of people… but I don’t really belong to any, actually. How I move is, I guess, convoluted for most people and
I know some people won’t understand how it can feel. I use h j k l and all the motions from vim described in the
third group (and even more; I full lecture of four hours would be required to explain all of them :D), but it all
depends on the distance I need to travel. If my cursor is on a word and I want to move to the beginning of a word
located very closely to my cursor on the same line, I’ll simply type www if it’s three words apart (or 3w if I’m
feeling funny). If the distance is higher, I use a tool called [EasyMotion].
Easymotion really is a wonderful tool. The idea is that it has several modes, depending on the kind of move you want to perform:
The way I typically do it is by mapping the three modes to <leader>l, <leader>w> and <leader>c (in my case,
<leader> is the space key).
Typing SPC l in my current buffer results in this:

Typing any highlighted character will make my cursor jump to it. The same applies for words, with SPC w:

For the character mode, after pressing SPC c, I have to press another character (the one I want to jump to). Let’s
say we want to jump to a # (which is not part of words): SPC c #:

This way of moving is not intuitive at first, but once you get used to it… it’s a must have.
Among all the things that I like about modal editing, here is a non-exhaustive list of features I expect to have around my fingers:
C-i and C-o: those allows me to jump to something / a file / a place in a buffer and then go back to where I was
right before with C-o (read it like out) or go back again with C-i (read it like in).t register with "tyi( (“put in the t
register the yank inside matching (), and paste that content later with "tp. Macros allow more powerful
editing control by assigning a key to set of actions with the q keyword (qa will register all the next keystrokes
and actions into the a macro). Then simply replay the macro with @a, for instance.d to delete, y to yank, c to change, t to go to the character right
before the one you search, % to go to the other delimiter, etc. And more complex text manipulation, such as “Let’s
change what’s inside this function parameter list, delimited by (”: ci(.It would take too much time to list everything, but the main idea is: I need the modal features when editing code.
So let’s talk about the list of editors I mentioned in the preface. The idea is to give my own opinion on those editors. What I like about them regarding the way I like to edit code and what I think is missing.
I currently use neovim in its TUI version, because it’s the most stable, fast and easy neovim experience out there so far to me. I have tried several GUI versions but never found what I was looking for — the main reason being that they pretty much all of them use Web™ technologies, and it’s a hard no to me. I think I should elaborate a bit more about that last point.
Why not using Web tech:
:profile to get that
value). Among those 23ms, coc.nvim take around 12ms.npm is one of the worst piece of software ever written. Please don’t make me use it again.So I use several plugins that I’m going to describe. I think it’s important that people know about all those because, in the collective mind, people still think vim / neovim are editors from the past. Well, not really.



fzf as a backend, so you get a very cool fuzzy search experience (with refined search, which I struggle
to find in any other editor — i.e. you can type something, then put a space and type again to match things
occurring before the actual match).ale, but this one is ace. It
basically provides you with LSP completion for lots of languages. It has an integrated marketplace you can use to
install new LSP servers and integrations, and it even has support for things completely unrelated (a bit
surprising, I think these should be standalone plugins), such as coc-explorer (which is a replacement of
NERDTree to me now), coc-snippets, etc.which-key from emacs, but for neovim. Basically, when setup properly, it will provide you with a visual list
of possible keybindings for sequences. I think it’s not that useful (or it is for people trying to use your
configuration or if you install a plugin that ships a lot of keybindings — which I truly dislike, btw), but it looks
pretty cool.ultisnips allows for a lot of room, like interpolation via shell, viml,
python, etc.fzf instead.vim-gitgutter so much; I love the colorizer
plugin a lot too. coc.nvim has been a blast so far (for most part). EasyMotion is typingporn to me. fzf is so
fast it should be illegal.:help pages. I think no other
software has such a cool set of help pages. Really, give it a try. You want to know how to configure coc.nvim?
Simply type :help coc-nvim.So I won’t going to be able to talk too much about this one, because I have only started using it at work (community edition). I’m using the vanilla one, with few modifications to none. I edit only Java with it.
The Java support is truly flawless. It does a lot of things for you, and among them, I was really impressed by:
var declarations but also
when passing arguments to functions.So this one is an important one, as it’s Microsoft’s take on editing. Everybody seems to love VS Code, and I get why. The UI is slick and fast — for a Web-based editor… ;). LSP support is obviously premium and flawless. It has a lot of community plugins, themes and integrations. For most part, even though it’s Web based, I have been enjoying it.
h to c — remember, bépo keyboard layout). It
simply doesn’t work.I have been using emacs lately (vanilla) as I saw a colleague using DOOM Emacs. I’m putting both in the same section as they are pretty similar. The way I see emacs and DOOM Emacs could be summed up with one word: united. I don’t know how they do that, but all plugins blend so well with each other. I’m using the ivy interface for completion and fuzzy search, and everything is so… well done. The UI is gorgeous, themes are awesome (I like the default dark theme, DOOM One), the editor is pretty fast — still slower than neovim to me, especially when scrolling, but still much faster than a Web-based editor.
which-key, for instance — that I now use in neovim.emacsclient to it, reducing the loading time to zero. Very brilliant and very useful!I’ll finish with atom, GitHub’s editor. I remember my first reaction when running atom for the first time was: “Woah, that’s a pretty editor.” The default colorscheme, One, is a universal colorscheme everybody knows. There are have been so many forks of it in so many different editors.
atom looks a lot like VS Code to me, but is a bit prettier in its UI — I do prefer atom’s UI to VS Code’s. The UI is slick and very clean. You can find a lot of extensions / plugins / themes, from LSP integrations to Markdown previews and Vim modes.
alt-gr (they call it altgraph in the config), that is not correctly recognized. It
sometimes work but I never recall the steps I have to do to fix the problem, and most of the time, I spend a
lot of times finding workarounds for something that just works in terminals.When I started coding, I remember of people talking about IDE / editor wars. Today, as I have tried a lot of editors, I can say that there’s no such thing as an editor war to me. All editors have drawbacks and picking the right editor is often a matter of taste and personal experience. I’m a keyboard lover (I make my own keyboards) and I truly love typing — not necessarily code, hence it was pretty obvious to go to emacs and vim back then (I actually started coding in emacs). Several years later, I joined the vim side and neovim. It’s only one year ago that I started looking at emacs again to see what has changed. And oh my. So many interesting things out there!
What I like about testing editors is that each and every editor has at least one killer feature no other has:
Spending some weeks in all those editors made me realize something about vim / neovim: I don’t necessarily think I should be using them, especially since I have used emacs / DOOM Emacs with its Evil-mode. Today, my thoughts revolve around the idea that a good neovim client could be a gtk application like emacs: slick, united, with great defaults and full support of neovim features, with support for gtk floating windows and popups (as it’s native in neovim and feels a bit hacky in TUI). We already have great plugins for things like git (fugitive / vim-gitgutter), completion and syntax highlighting with coc.nvim / vim-lsp / vim-treesitter. The only piece that is missing to me is a great GUI to leverage the “hacks” we have to do in TUI to actually use real popups, “balloons”, etc. Once we get a decent neovim GUI, I think I will have found my favorite editor of all time.
Until then, I’ll stick around neovim TUI as it’s what is as close at what I would like to get. I hope this article will have given a bit of hints about what a vim / neovim enthusiast might expect from a modern editor. And I say a vim enthusiast, not all of them. We all look for different things and I truly thing we live in a wonderful world with all those editors out there. They won’t fit for everybody, but everybody will find one for them, and damn, that’s a pretty good conclusion to this article.
Feel free to tell me about what you think about your editor, whether there’re things you dislike about it or what you would like to see appear in it. Keep the vibes!
]]>&mut) to a typed context
when loading and reloading a resource. This enables per-value loadings, which is neat if you need
to add extra data when loading your resources (e.g. increment a counter).Load, in order to tell
warmy how to load a given object of a given type. This is convenient but carries a bad drawback:
if you want to represent your object with both JSON and XML for instance, you need to type wrap
your type so that you can impl Load twice. This was annoying and the type system also handed you
back an object which type was the wrapper type, not the wrapped type. This annoyance was
removed in 0.7 as you now have an extra type variable to Load – it defaults to () though –
that you can use to impl Load several times – think of that tag type variable as a type
representing the encoding or the method to use to load your value./splines/color_grading.json. Before that, you still had to
provide a real filesystem path, which was both confusing and annoying (since you already give one
when you create a Store, the object that holds your resource).I posted on reddit in order to make people know of the new version, and interesting talks started to occur on both IRC and GitHub. What people seem to want the most now is asynchronous loading and reloading. I’ve been wanting that feature for a while too so I decided it could be a good idea to start working on it. However, after a day of toying around, I came to the realization that I should write a small blog post about it because I think it’s not trivial and it could help me shape my ideas.
Note: this post is about brainstorming and setting up the frame to why and how async warmy. You will find incomplete code, ideas and questions there. If you want to contribute to the discussion, you’re more than welcome!
What does it mean to have a synchronous computation? What does it mean to have an asynchronous one? You might find it funny, but a lot of people are still confused with the exact definition, so I’ll try to give you more hindsight.
We talk about a synchronous task when we have to wait for it to finish before moving on to another task. We have to wait until its completion to get the control flow back and call other functions. We often talk about blocking computations, because you cannot do anything else while that computation is running – at least on the thread this computation is running on.
We talk about an asynchronous task when you can get the control flow back without having to wait
for the task to finish. However, that doesn’t necessarily mean that the task is being executed in
parallel or concurrently. At some extent, you could easily label generators as asynchronous
primitives, and this is what actually happens in async / await code: you give back the control
flow to the caller and the callee execution gets re-scheduled later. Hence, this is asynchronous
programming, yet the scheduling execution could be implemented on a single thread – hence no
parallel nor concurrency actually happen. Another example is when you perform a HTTP request: you
can send the request and instead of blocking until the response arrive, you can give the control
back, do something else, and then, at some time, handle the response. You don’t need parallelism
to do this: you need asynchronous programming.
Note: a generalization of a generator is a coroutine, which hands the control flow back to another coroutine instead of the caller when wanted.
At the time of writing this blog entry, warmy is completely synchronous. Consider the following example:
extern crate serde_json;
extern crate warmy;
use std::error;
use std::fmt;
use std::fs::File;
use std::io;
use std::thread;
use std::time::Duration;
use warmy::{FSKey, Load, Loaded, Res, Store, StoreOpt, Storage};
struct JSON;
#[derive(Clone, Copy, Debug, PartialEq)]
struct Foo {
array: [f32; 4]
}
#[derive(Debug)]
enum FooError {
JsonError(serde_json::Error),
IOError(io::Error)
}
impl fmt::Display for FooError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
FooError::JsonError(ref e) => e.fmt(f),
FooError::IOError(ref e) => e.fmt(f),
}
}
}
impl error::Error for FooError {
fn description(&self) -> &str {
match *self {
FooError::JsonError(ref e) => e.description(),
FooError::IOError(ref e) => e.description(),
}
}
fn cause(&self) -> Option<&error::Error> {
match *self {
FooError::JsonError(ref e) => Some(e),
FooError::IOError(ref e) => Some(e),
}
}
}
impl<C> Load<C, JSON> for Foo {
type Key = FSKey;
type Error = FooError;
fn load(
key: Self::Key,
_: &mut Storage<C>,
_: &mut C,
) -> Result<Loaded<Self>, Self::Error> {
let file = File::open(key.as_path()).map_err(FooError::IOError)?;
let array = serde_json::from_reader(file).map_err(FooError::JsonError)?;
let foo = Foo { array };
Ok(foo.into())
}
}
fn main() {
// read scarce resources in /tmp
let ctx = &mut ();
let opt = StoreOpt::default().set_root("/tmp");
let mut store: Store<()> = Store::new(opt).expect("store creation failed");
// get our resources via JSON decoding
let foo: Res<Foo> = store.get_by(&FSKey::new("/foo.json"), ctx, JSON).expect("foo should be there");
let foo2: Res<Foo> = store.get_by(&FSKey::new("/foo2.json"), ctx, JSON).expect("foo should be there");
println!("{:#?}", foo);
println!("{:#?}", foo2);
loop {
// sync the resources with the disk
store.sync(ctx);
thread::sleep(Duration::from_millis(100));
}
}
Note: you can test that code by inserting
warmy = "0.7"andserde_json = "1"in aCargo.tomland by creating the JSON files containing 4D arrays of numbers, like[0, 1, 2, 3]in/tmp/foo{,2}.json.
If you look closely at that example, you can spot two important locations when we get resources: when
we create the foo and foo2 bindings (search for store.get_by invocations). Here, it’s important
to understand what’s really happening:
/foo.json is loading on the current thread./foo2.json starts loading.We can already see a pity here: both the files are completely disconnected from each other, yet, the second file must wait for the former to finish before starting loading. We’re not using all the cores / threads of our CPU. So we could perfectly imagine this new scenario:
/foo.json and immediately get control flow back./foo2.json and immediately get control flow back.You could even do whatever you want between (2.) and (3.). The point here is that we can run tasks without having to wait for them to finish before starting others. The explicit waiting part could be a blocking call, such as:
let foo_task = store.get_by(…);
let foo2_task = store.get_by(…);
let foo = foo_task.wait();
let foo2 = foo2_task.wait();
Note: this is not the foreseen or planned interface. It’s just there to illustrate what we want to achieve there.
The goal of this blog entry is to explore possible implementations.
The futures crate is a very promising and interesting crate. What it provides is basically a mean to express data that might get available in the future. For instance:
fn ping(host: &str) -> ???
Here, ping is a function that will perform an ICMP request to a given host, identified by the
function argument. Because that function might take a while (at least several milliseconds, which is
a lot), we have to make a decision:
ping function and do something else
until the response arrive.If you’ve followed all what I said since the beginning of this blog post, you might have noticed that (1.) is synchronous and (2.) is asynchronous (also notice that we haven’t talked about parallelism yet!).
With (1.), we would write ping’s signature like this:
fn ping(host: &str) -> Result<PingResponse, NetworkError>
With (2.), we would write it like this, using the futures crate:
fn ping(host: &str) -> impl Future<Item = PingResponse, Error = NetworkError>
Note: the syntax
impl Traitis called conservative impl trait and its description can be found in the corresponding RFC on impl Trait.
So what you basically do here is to send a non-blocking request and get its result in a non-blocking way. Sweet! How does that apply to warmy?
Asynchronous warmy should have the following properties:
Store::sync function.The current definition of the Store::get function
is:
pub fn get<K, T>(
&mut self,
key: &K,
ctx: &mut C
) -> Result<Res<T>, StoreErrorOr<T, C>>
where
T: Load<C>,
K: Clone + Into<T::Key>
We want something like this:
pub fn async_get<K, T>(
&mut self,
key: &K,
ctx: &mut C
) -> Result<impl Future<Item = AsyncRes<T>, Error = StoreErrorOr<T, C>>, StoreErrorOr<T, C>>
where
T: Load<C>,
K: Clone + Into<T::Key>
Woosh. It gets hard to read – eventually, trait aliases will help us there. We can see the use of
the futures crate here, in the return type:
impl Future<Item = AsyncRes<T>, Error = StoreErrorOr<T, C>>
Which reads as “An asynchronous resource of type T available in the future or an error due to the
loading of T and C or due to the store”.
However, something important to notice is that we miss a crucial point here: we actually want a parallel execution. We could start try by defining a small state machine to step through the process of loading a resource. That could be:
enum LoadState<K, T, C> {
Pending(K, Shared<C>), // resource loading pending
Loading(Receiver<AsyncRes<T>>), // resource currently loading
Done, // resource loaded
}
This small state machine can be stepped through by the following process:
fn start_load<K, T, C>(key: K, ctx: &mut C) -> impl Future<Item = LoadState<K, T, C>> {
LoadState::Pending(key, ctx.share_somehow())
}
impl<K, T, C> Future for LoadState<K, T, C> {
type Item = AsyncRes<T>;
type Error = StoreErrorOr<T, C>;
fn poll(&mut self, ctx: &mut Context) -> Result<Async<Self::Item>, Self::Error> {
match *self {
LoadState::Pending(..) => {
// start the loading
let (sx, rx) = oneshot::channel();
let task = Task::new(move || {
// load the resource somehow
let res = …;
sx.send(res);
});
// spawn a new task to load the resource and update the state machine
ctx.spawn(task);
*self = LoadState::Loading(rx);
Ok(Async::NotReady)
}
LoadState::Loading(ref mut rx) => {
match rx.map(|res| {
*self = LoadState::Done;
res
}).poll()?
}
LoadState::Done => panic!("poll called after future has arrived")
}
}
}
That code might be incomplete or not typecheck completely (not tested), but the idea is there. I
still don’t know whether this is the good way to use futures or whether I should run a single, long
computation without the small state machine. I just don’t know yet.
This part is tightly bound to how the Store::sync will behave. Now that I tink about it, maybe it’d
be more ergonomic / elegant / simpler to have an event loop living on a separate thread that would
benefit from epoll and all those fancy asynchronous primitives to deal with that. That would result
in no more by-hand synchronization, since it’d be done by the event loop / reactor / whatever you
want to name it.
Currently, I have not settled up on any decision regarding reloading.
This will be definitely a tricky part as well, because what was before:
store.sync(&mut ctx);
will then become something like:
store.sync(ctx.share());
Also, something that might happen is that because the synchronization will be done in an asynchronous way, you will need to ensure that you do not try to synchronize the same resource several times.
Finally, if you take into account the previous section, the user might not even synchronize by hand anymore, so the context will have to be shared anyway and moved to the event loop. Argh. That doesn’t seem simple at all! :)
One final aspect of asynchronous warmy is obviously how we’re going to poll the various Futures
generated by the get functions. One first, naive idea would be to run those in OS threads. However,
imagine that you’ll be loading plenty of resources (on a big and release project, it’s easy to
imagine several thousands of resources loading in parallel). You don’t want to allocate that many OS
threads but instead rely on a green threading solution… which I haven’t found yet. People on IRC
advised to have a look at futures-cpupool, but I’d like to have a complete solution set before
deciding anything.
I will post this blog post on reddit with hope that people will come up with ideas and I’m sure enlighting ideas on how to cope with all of this asynchronous properties I want to push to warmy.
Thanks for having read me, and keep the vibes!
]]>al 0.1.1.2 was shipped. It includes several improvements, among them:
stdcall flag is available ;alcIsExtensionSupported was renamed to alcIsExtensionPresent as the
former just doesn’t exist – sorry for the typo.The stdcall flag might be a great ally for people compiling on a 32-bit
Windows. For people on 64-bit Windows, the default is sufficient – and for
UNIX systems, you don’t have anything special to do.
I’m also looking for hackers to test the library on the most OS as possible. I have issues with it (see this) and I’d like to hear from people. Even though I have that issue, al compiles well and runs great in ghci, which is weird regarding the fact running an application compiled with al silently crashes at startup.
That’s a nasty issue I don’t really know how to correctly fix. Up to now,
default OpenAL 1.1 SDK installation are detected for Windows. For people with
custom installation and other systems, you have to pass the path of the SDK by
hand. I know, it’s a pain in the ass, but I don’t want to depend on tools like
pkg-config as that tool is not available everywhere – getting it on Windows is
not that simple.
Uniforms are a way to pass data to shaders. I won’t talk about uniform blocks nor uniform buffers – I’ll make a dedicated post for that purpose. The common OpenGL uniform flow is the following:
glGetUniformLocation, or you can use an explicit location if you want to handle the semantics
on your own ;glProgramUniform.You typically don’t retrieve the location each time you need to send values to the GPU – you only retrieve them once, while initializing.
The first thing to make uniforms more elegant and safer is to provide a typeclass to provide a
shared interface. Instead of using several functions for each type of uniform – glProgramUniform1i
for Int32, glProgramUniform1f for Float and so on – we can just provide a function that will
call the right OpenGL function for the type:
class Uniform a where
sendUniform :: GLuint -> GLint -> a -> IO ()
instance Uniform Int32 where
sendUniform = glProgramUniform1i
instance Uniform Float where
sendUniform = glProgramUniform1f
-- and so on…
That’s the first step, and I think everyone should do that. However, that way of doing has several drawbacks:
sendUniform pretty much everywhere ;sendUniform? If we haven’t sent the
uniform yet, we might have an undefined behavior. If we already have, we will override all
future draws with that value, which is very wrong… ;In my luminance package, I used to represent uniforms as values.
newtype U a = U { runU :: a -> IO () }
We can then alter the Uniform typeclass to make it simpler:
class Uniform a where
toU :: GLuint -> GLint -> U a
instance Uniform Int32 where
toU prog l = U $ glProgramUniform1i prog l
instance Uniform Float where
toU prog l = U $ glProgramUniform1f prog l
We also have a pure interface now. I used to provide another type, Uniformed, to be able to
send uniforms without exposing IO, and an operator to accumulate uniforms settings, (@=):
newtype Uniformed a = Uniformed { runUniformed :: IO a } deriving (Applicative,Functor,Monad)
(@=) :: U a -> a -> Uniformed ()
U f @= a = Uniformed $ f a
Pretty simple.
The problem with that is that we still have the completion problem and the side-effects, because we
just wrap them without adding anything special – Uniformed is isomorphic to IO. We have no way
to create a type and ensure that all uniforms have been sent down to the GPU…
If you’re an advanced Haskell programmer, you might have noticed something very interesting
about our U type. It’s contravariant in its argument. What’s cool about that is that we could then
create new uniform types – new U – by contramapping over those types! That means we can enrich
the scope of the hardcoded Uniform instances, because the single way we have to get a U is
to use Uniform.toU. With contravariance, we can – in theory – extend those types to all types.
Sounds handy eh? First thing first, contravariant functor. A contravariant functor is a functor that flips the direction of the morphism:
class Contravariant f where
contramap :: (a -> b) -> f b -> f a
(>$) :: b -> f b -> f a
contramap is the contravariant version of fmap and (>$) is the contravariant version of
(<$). If you’re not used to contravariance or if it’s the first time you see such a type
signature, it might seem confusing or even magic. Well, that’s the mathematic magic in the
place! But you’ll see just below that there’s no magic no trick in the implementation.
Because U is contravariant in its argument, we can define a Contravariant instance:
instance Contravariant U where
contramap f u = U $ runU u . f
As you can see, nothing tricky here. We just apply the (a -> b) function on the input of the
resulting U a so that we can pass it to u, and we just runU the whole thing.
A few friends of mine – not Haskeller though – told me things like “That’s just theory bullshit, no one needs to know what a contravariant thingy stuff is!”. Well, here’s an example:
newtype Color = Color {
colorName :: String
, colorValue :: (Float,Float,Float,Float)
}
Even though we have an instance of Uniform for (Float,Float,Float,Float), there will never be an
instance of Uniform for Color, so we can’t have a U Color… Or can we?
uColor = contramap colorValue float4U
The type of uColor is… U Color! That works because contravariance enabled us to adapt the
Color structure so that we end up on (Float,Float,Float,Float). The contravariance property is
then a very great ally in such situations!
We can even dig in deeper! Something cool would be to do the same thing, but for several fields. Imagine a mouse:
data Mouse = Mouse {
mouseX :: Float
, mouseY :: Float
}
We’d like to find a cool way to have U Mouse, so that we can send the mouse cursor to shaders.
We’d like to contramap over mouseX and mouseY. A bit like with Functor + Applicative:
getMouseX :: IO Float
getMouseY :: IO Float
getMouse :: IO Mouse
getMouse = Mouse <$> getMouseX <*> getMouseY
We could have the same thing for contravariance… And guess what. That exists, and that’s called
divisible contravariant functors! A Divisible contravariant functor is the exact contravariant
version of Applicative!
class (Contravariant f) => Divisible f where
divide :: (a -> (b,c)) -> f b -> f c -> f a
conquer :: f a
divide is the contravariant version of (<*>) and conquer is the contravariant version of
pure. You know that pure’s type is a -> f a, which is isomorphic to (() -> a) -> f a. Take
the contravariant version of (() -> a) -> f a, you end up with (a -> ()) -> f a. (a -> ()) is
isomorphic to (), so we can simplify the whole thing to f a. Here you have conquer. Thank you
to Edward Kmett for helping me understand that!
Let’s see how we can implement Divisible for U!
instance Divisible U where
divide f p q = U $ \a -> do
let (b,c) = f a
runU p b
runU q c
conquer = U . const $ pure ()
And now let’s use it to get a U Mouse!
let uMouse = divide (\(Mouse mx my) -> (mx,my)) mouseXU mouseYU
And here we have uMouse :: U Mouse! As you can see, if you have several uniforms – for each fields
of the type, you can divide your type and map all fields to the uniforms by applying several times
divide.
The current implementation is almost the one shown here. There’s also a Decidable instance, but
I won’t talk about that for now.
The cool thing about that is that I can lose the Uniformed monadic type and rely only on U.
Thanks to the Divisible typeclass, we have completion, and we can’t override future uniforms then!
I hope you’ve learnt something cool and useful through this. Keep in mind that category abstractions are powerful and are useful in some contexts.
Keep hacking around, keep being curious. A Haskeller never stops learning! And that’s what so cool about Haskell! Keep the vibe, and see you another luminance post soon!
]]>I’m very happy about people getting interested about my luminance graphics framework. I haven’t received use case feedback yet, but I’m pretty confident I will sooner or later.
In the waiting, I decided to write an embedded tutorial. It can be found here.
That tutorial explains all the basic types of luminance – not all though, you’ll have to dig in the documentation ;) – and describes how you should use it. I will try to add more documentation for each modules in order to end up with a very well documented piece of software!
People on reddit complain – they are right to – about the fact the samples just “didn’t work. They actually did, but the errors were muted. I released luminance-0.1.1 to fix that issue. Now you’ll get the proper error messages.
The most common issue is when you try to run a sample without having the required hardware
implementation. luminance requires OpenGL 4.5. On Linux, you might need to use primusrun or
optirun if you have the Optimus technology. On Windows, I guess you have to allow the samples
to run on the dedicated GPU. And on Mac OSX… I have no idea; primusrun / optirun, I’d go.
Anyways, I’d like to thank all people who have/will tried/try the package. As always, I’ll keep you informed about all the big steps I take about luminance. Keep the vibe!
]]>A huge amount of features, bug fixes, usability / comfort enhancement, new crates and architecture redesign are gathered in that update. First, let’s talk about the new most important feature: the redesign.
Before 0.40, luminance was an OpenGL 3.3 crate. If you’re not into graphics APIs, OpenGL is a graphics normalized specification, built by an open group (Khronos) that has been around for decades. Because it’s an open specification, lots of vendors can implement and provide drivers to have OpenGL support, but also Free and OpenSource projects. The 3.3 version was old enough to support a wide variety of devices, even old ones, while still having interesting features allowing to do lots of useful things.
However, as time passes, new devices and technologies emerge. WebGL 1.0, which appeared in 2011, is to the Web what OpenGL is to desktop and console graphics programming. It is an open API browsers can implement to provide support for accelerated 2D/3D rendering in your browser.
WebGL 2.0, which appeared more recently, in 2017 (partial support in some browsers), provide an API almost identical to what you find in OpenGL 3.3 (with some tricky caveats).
I’ve been wanting to have a way to write “luminance code”, and have it compile for other platforms than desktops, like Web and mobile (Android and iOS). With the previous architecture, since luminance was bound to OpenGL 3.3, it was not possible. Here comes the new archicture.
The whole luminance crate was rewritten so that its public-facing interface is completely agnostic
of what executing GPU code is about. No OpenGL anymore. Instead, it depends on a type variable,
referred as B in luminance’s types, which must satisfy a set of conditions to be considered a
backend.
A backend is an implementation of the expectations made on the public-facing interface. For
instance, you can set a GPU buffer by providing an index and a value. That’s the public facing
part. However, what happens on the GPU is left to the backend implementation.
This new way of doing split the luminance crate into several ones:
GL33 — which
can be passed around luminance to select the OpenGL 3.3 backend.Added to this, because of how generic and abstract luminance now is, people might get issues when
trying to write code that will work whatever the backend. When using the set functions on a
buffer, it is required that the type implements a trait from luminance —
luminance::backend::buffer::Buffer. So it’s likely the user will have to constrain types and it
might get boring. For this reason, a new crate was created: luminance-front.
luminance-front allows people to write code using types from luminance that got their B type
variable replaced by a backend type at compile-time. This is done by inspecting the compilation
target, and features you enable. You are then free to write your code without worrying about
whether traits are implemented, since luminance-front takes care of that for you. The
re-exported types are simple aliases to luminance’s types, so your generic code — if you write
any — will be compatible.
The changelog is quite massive for a single update. I wish I had the opportunity to split it into several updates, but the redesign meant a lot of changes, and I got several very interesting PRs and feature requests. Among very new stuff:
Program or Tess.Tess, a BIG update has landed, has it’s now heavily typed (vertex type, index type,
vertex instance data type, memory interleaving type).A huge thanks to all the people who contributed. It means a lot to me! The list of changes is big, so I’ll leave you with the changelog here.
Disclaimer: this is the luminance changelog, but all the crates have their own, too.
Because that update is massive and has some breaking changes, I have also decided to include a migration guide. The migration guide is a section in luminance’s changelog to explain all the things you need to do to migrate from luminance-0.39 to luminance-0.40. Please: if you find something that is missing or you’re struggling with, please open an issue, a PR, or ping me on whatever platform you like.
The Tess type now supports slicing deinterleaved memory in very, very elegant ways, checked
at compile-type and type-driven. The whole new Tess type design is so new that a blog article
is on its way about that topic, as I find it very interesting.
While testing the redesign and new features, I have implemented a Conway’s Game of Life for fun — no code available yet. I plan to remake it from scratch and record the whole experience on video. That will be a new format and I want to hear from people and know whether they like the format.
Finally, the book was also updated. The three chapters have been updated and some features have been added — like aspect ratio in chapter 3; I don’t understand why I haven’t talked about that earlier!
The next steps for luminance and myself, besides the blog articles and video, are… getting some rest. I plan to get back to demoscene production, and I’m 100% sure there’s already a lot to do with that current release of luminance. I plan to experiment with new backends as well, such as an OpenGL 2.0 backend (if possible), to support really old hardware; an OpenGL 4.6 backend for modern hardware (easy), as I already know that API pretty well; an OpenGL ES backend for mobile development, and later, a Vulkan and, perhaps, a WebGPU backends.
Stay tuned for the Tess article, the Game of Life video… and keep making super fun video games,
animation and toys with luminance!
And keep the vibes!
]]>In the past two years, many projects of mine got more and more contributions.
Some of those projects, such as This Week in Neovim (that I gave away),
kak-tree-sitter and others received many contributions. As I reviewed
contributions, I realized that I do not really enjoy the tools used to implement
the process. That is, GitHub. Moreover, I have a special thing against
Microsoft (in)famous way of doing things in (c.f. EEE). So, why is it such a
big deal to me, and what is the alternative?
git (I don’t think I need to explain what it is, right?) was created by Linus
Torvalds (again, I don’t think I need to present him) to solve the BitKeeper
dispute. Back then, people were using CVS or SVN, and something like BitKeeper
was a centralized place where the kernel was developped.
In 2005, after the decision to stop providing free BitKeeper copies to the
kernel community, Linus created git. He created git to provide the free and
open-source software community with a versioning tool working on a
decentralized model. Indeed, with git, teams can organize the way they want
and exchange patches and commits without having to depend on a third party.
For instance, if Alice and Bob want to work on a project together and version
it via git, they do not need to rely on anyone else but themselves. They can
send commits to each other with git push or git pull (granting they
configure their ssh correctly). They can use their university server to host
a bare git repository and synchronize there. They can exchange patches by mail.
Etc.
Now, something else they could do would be to use GitHub. Or GitLab. Or BitBucket. In the end, it’s the option mentioned above: a repository always up for people to send commits to and receive commits from. However, I do see one weird pattern here. Using GitHub, for instance (but really, it’s the same for most others) is basically the same as taking a decentralized tool and putting it in jail. If someone wants to contribute to Alice and Bob project, they have to create a GitHub account, and abide by the GitHub rules. More interesting, GitHub is not a free and open-source project anymore. It’s owned by Microsoft. Alice and Bob projects are physically stored on a Microsoft server. Given the history of Microsoft in the FOSS world, I can imagine how it would cause a moral problem.
Another point that is specific to GitHub: Copilot. I think Copilot is FOSS abomination. The reason is that, even though your code is under a copyleft — or even a copyright, GitHub terms of service is pretty explicit about the hosted content: they can use it to improve their own products. This section for instance. I don’t know about you, but to me, it’s grey zone there. I do not like the idea that my code can be “analyzed” by Microsoft to enhance Copilot. All of my projects are licensed under a copyleft (BSD-3) that mentions that my name should be mentioned when my code is used, and other clauses that I think are violated by this Copilot horror.
And I’m not even talking about Microsoft Copilot Pro, which is a product. That you have to pay for. That is made from public, free and open-source contributions from millions of projects. Fuck that.
Finally, those platforms often host many different things that people will start using and highly depend on. For instance, bug tracker, wikis, project management, etc. Even though this is not the main problem to me, I like using something that was made to solve a single problem. Having a giantic platform solving basically “How to do software development” is something that now sounds like a red flag to me. What happens when you decide to move on? Well, you have to move everything. And it’s my case today.
Something like two years ago, someone on IRC praised the email-based git workflow. Wait, email-based? In 2022? Isn’t that a bit too hardcore? Well, there is one really big misconception about emails: all-in-one providers.
Like many other people, I do use a webmail. If it’s not GMail, it’s Outlook or something else. GMail is hard not to notice, because in the professional world, every companies use it. It’s easy for them to manage and you get all the important things at once — swarm of newcomer welcoming emails, meetings, various meetups, etc. All in one. The problem with that is that it requires you to go to your browser. And the online experience is honestly pretty bad — it always has been to me. I try to spend as few times as possible in GMail, because I don’t really like the interface.
So how can someone use emails to work with git? Well, from the beginning,
git was made with the email workflow on mind. The idea is simple:
git send-email command —
or git format-patch and manually send the patch, but really, you shouldn’t
have to do that. You typically send that email to a development mailing list.git am.The main misconception part is about the review part. On GitHub, reviewing is
nice, because you can comment in line. In emails… well, it’s exactly the same,
and even better. What people usually do is to reply to your email by quoting it,
and dissect the email section by section. For instance, imagine I have a project
with a README.md file containing a single line:
Hello. My name is Dimitri and this is a README.
Let’s say I modify that file to hold this content instead:
# Hello world!
My name is Dimitri and this is a README.
I make a commit with git commit -am "Add hello section to README.md.". When
using git send-email / git format-patch, it will generate something like
this patch:
From 020d9085a35d6c34bc1b7e86b78f257c4d556bbb Mon Sep 17 00:00:00 2001
From: Dimitri Sabadie <[email protected]>
Date: Thu, 30 May 2024 14:28:11 +0200
Subject: [PATCH] Add hello section to README.md.
It looks better to me and allows to demonstrate the email workflow a bit more.
---
README.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 72ae80f..70341da 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,3 @@
-Hello. My name is Dimitri and this is a README.
+# Hello world!
+
+My name is Dimitri and this is a README.
--
2.45.1
The first 4 lines are part of the email, so when receiving your email, the client of the people reading the mailing list should format all that correctly. The rest is the email content! Someone could reply something like:
> -Hello. My name is Dimitri and this is a README.
> +# Hello world!
Thank you, we needed that header.
> +My name is Dimitri and this is a README.
May you add more info to include other maintainers as well? Alice and Bob could
provide more details there.
Cheers,
Dimitri
The author of the patch can then iterate and send another email (with the -v2
annotation, because it would be the next version; -v3 for the next iteration,
etc.). The advantage here is that someone can reply to that first review by just
discussing the first part, quoting it:
> > -Hello. My name is Dimitri and this is a README.
> > +# Hello world!
>
> Thank you, we needed that header.
I think we should also include a sub-header describing something else.
While at the same time, someone can discuss the second part of the change:
> > +My name is Dimitri and this is a README.
>
> May you add more info to include other maintainers as well? Alice and Bob could
> provide more details there.
Yes, like email addresses and PGP signatures, please.
Those will create a tree of emails, where each sub-thread is scoped to a specific part of your patch. Such a feature is not possible in GitHub or any other centalized platform.
So the only important thing is to use a good client. I personally use aerc, along with some other tools to synchronize IMAP maildirs and send SMTP messages. The configuration is not that hard once you get the hang of it.
Sending an email to contribute is nice, but where to send the email? Well, it
depends on the project. For that, I think that every open-source projects should
have a CONTRIBUTING.md file explaining where to send patches. I use
SourceHut, which provides me a place to host my repository and mailing lists.
The rest is done entirely outside of SourceHut. Especially, even though you can
create an account on SourceHut, you don’t have to. You don’t have to in
order to:
That part here makes me smile, because it makes so much more sense to me. People can contribute with their email address. They do not need to engage with the platform to start contributing, which is nice and more decentralized to me. One can even send a patch to a maintainer directly if they really want.
I think the problem with the email-based workflow is that you don’t exchange commits anymore, but patches. There is a slight difference.
A patch is a set of changes to be applied. It contains some metadata, like the parent commit to apply to, the expected SHA1, some diff-stats, and that’s pretty much all.
A commit is a patch with more metadata, like the committer / author identities, a commit message, the parent commit, a signature, etc. etc.
A commit always refers to at least one parent (unless it was the first commit).
When you send a patch over email, the maintainer that applies the patch does it
with git am, which turns the email content into a commit. The message commit,
for instance, is taken from the mail body. However, there is one information
that is lost during the process: the signature.
Indeed, when you work on your local copy of the repository, you might sign your commits with your PGP key. When you decide to send the commits as patches to a development list, all of the metadata from the commits are ripped. That means that the signature is simply lost.
For most people, in terms of security, it’s not that bad. Patches are accepted
from emails, and you can (you should!) sign your email with your PGP key. As a
maintainer, my email client (aerc) shows me the signature. If you don’t sign
your email, I cannot certify it’s you sending me the email. Most people don’t
seem to care that much about authorship, but I do. I think it’s important to be
able to retrace that a patch which identity is [email protected] was really provided
by that person. A signature doesn’t prove you wrote the patch, but it proves
you submitted it, the same way that signing a commit means that you integrated
that commit into the repository.
Currently, there is no way to do that on the email workflow and I find that a bit of a problem. There should be an alternative, like manually signing the patch. The problem is that, if the maintainer has to use a three-way merge, it will invalidate both the parent commit’s SHA1 and the expected SHA1 of the patch, invalidating the carried signature.
Even though the email workflow has some problems, having the contributions locally and applying patches locally is a much more enjoyable process. The first thing is that sending many commits ends up in a nice thread of mails. It’s super easy to just open the thread locally and apply the patches. The only concern I have is authorship, but people told me I might be the only person caring about that — am? All in all, this email-based workflow allows me to do everything from my terminal, and even tickets (todos in sourcehut) and bug reports / feature requests are done via email. It just feels good.
]]>Type aliasing is something that’s been included in most languages for a while
now. Basically, if you have a type Foo, aliasing Bar to Foo means that
you introduce a new symbol Bar that refers to Foo. It’s not a new type,
it’s just an alias, a binding.
In Haskell, we do aliasing with the keyword type, which is a bit stupid
since we won’t necessarily alias a type (we could alias a function, a typeclass
constraint, and so on and so forth).
Aliasing is great and useful in many ways. For instance, there’s a Haskell
package I love called linear. That
package is used to deal with linear algebra, and you can find common math types
in there among vectors (Vec) and low-algebra vectors (V2, V3, V4). There
are the same things for matrices (M22 for 2x2 matrices, M33, M44).
If you come up with using 4x4 floating matrices a lot, you might end up with a
lot of M44 Float everywhere in your code. So that would be sane aliasing it.
Like so:
type V4F = V4 Float
This is a perfect aliasing use to me as V4F is straight-forward for
graphics people. Furthermore, linear is pretty a standard now in Haskell
linear algebra application, so V4 should be familiar to you.
However, there is a drawback with type aliasing.
I spent more than a day crawling for a bug. I was refactoring code, a lot of code. Nothing fancy though, just some code cleaning. Nevertheless, I created a brand new git branch to make my cleaning. On the initial branch, the application behavior was normal. After having cleaned the code, I got a black screen.
After a long investigation, I found out that the bug was due to a mismatch
between two matrices – i.e. M44 Float. One matrix is used as a camera
projection and the other one is used as a light projection. In term of code,
both matrices are placed in a type. I simplified (a lot), but this is the idea:
data Foo = Foo {
-- […]
, cameraProjection :: M44 Float
, lightProjection :: M44 Float
}
What makes those matrices different in term of compilation? Nothing.
M44 Float and M44 Float are the same type. Swapping matrices is then
silently compiled, since it’s not an error to the compiler. And such a swapping
result in a terrible and hard to find bug.
Yeah, so we could have simply aliased those matrices to two different names!
Remember what I said earlier. Aliased types are not new types. They’re bindings in the type system:
type CameraProjection = M44 Float
type LightProjection = M44 Float
Here, both CameraProjection and LightProjection are the same type. If a
function expects a CameraProjection, it actually expects a M44 Float. You
could then pass a value of type M44 Float or even LightProjection, that
would be legit.
Is there a solution yet?
Yes, there is. Haskell has several keywords to create types:
type, which is, as we’ve already seen, a way to alias types;data, which is used to create data types;newtype, which is used as a type wrapper to introduce a data type.newtype is the most important keyword for us here. Basically, it has the same
semantic as data except that newtype introduces a type with a single
field; it can’t have none nor several. newtype is often used to express the
same thing that the inner type, but adding / removing some properties.
For instance, Bool can’t be an instance of Monoid since we could define a
lot of instances. mempty could be True if we consider the monoid of
booleans with AND but it would be False if we consider the monoid of
booleans with OR. Well, we could write those, actually:
newtype And = And { getAnd :: Bool } deriving WhateverYouWant
newtype Or = Or { getOr :: Bool } deriving WhateverYouWant
instance Monoid And where
mempty = True
And a `mappend` And b = And (a && b)
instance Monoid Or where
mempty = False
Or a `mappend` Or b = Or (a || b)
Those two types are defined in Data.Monoid as All and Any.
Back to our problem. We could use newtype to add M44 Float the correct
semantic, and disable us to mix them:
newtype CameraProjection = CameraProjection { getCameraProjection :: M44 Float }
newtype LightProjection = LightProjection { getLightProjection :: M44 Float }
And here, CameraProjection ≠ LightProjection. If a function expects a
CameraProjection, you can’t pass anything else than a CameraProjection.
However, be careful. You’ll have to wrap a M44 Float into your
CameraProjection. Such a function, like the CameraProjection constructor, is
not semantic safe since you could still pass a light representation.
Nevertheless, it’s better than the initial design since once wrapped, you can’t
make confusions anymore.
To sum up, use aliasing when it’s handy but don’t use it if it could break
semantics or add confusion. If you have several types that use the same
type (e.g. colors and positions could use the same V3 Float type), don’t
use aliases! Create proper types to prevent confusion later on. Your compiler
will reward you, then you don’t get nasty runtime bugs ;).
Most of the things I’m going to show here in Kakoune are also doable in Helix, but Helix is not as mature as Kakoune, so it’s likely some features won’t be available at the time of writing this blog article, May 2025.
Disclaimer: as always with programming tools, use the tool you are comfortable with. This article only reflects the way I see editing; it doesn’t mean one tool is superior to another in an absolute way. It’s just a way for me to share why I think Kakoune (Helix) is better, and provide some hindsight on both editors.
Thank you: thank you
@Screwtapefor reviewing this article!
The example is this:
public enum TranslationCodeId
{
// buttons
BUTTON_Accept,
BUTTON_Cancel,
// labels
LABEL_Actions,
LABEL_Column,
// messages
MESSAGE_Confirm,
MESSAGE_GoBack,
// more fields...
}
Vim does it with two different actions:
0 to all enum fields: :g/,$/v;//;norm $i = 0.vi}o2/0,$<Enter>g<C-a>.How to do that with Kakoune? The first thing is to be able to get a selection on each enum field. We can use
a similar trick as what Vim does by targetting the line ending with a , with s,$. Because we assume — like
in Vim — that this enum definition is the whole buffer, we can work on the whole buffer with the % key
(similar to the g/ of Vim). So we simply just type %s,$<ret>.

From now, the rest is really trivial, as we have a selection on each ,. We can then simply type i = <c-r>#,
which will enter = N before every column, where N will have the right value.


Some explanations: pressing control-r will allow the next key to select a register that will be pasted after all selections. Kakoune has many built-in registers, and
#is the selection index register; it gives you the index of each individual selection, starting from1, ascending.
This one wants us to fix the ending characters of some lines:
INSERT INTO AdminTranslationCodeText
(AdminTranslationCodeTextId, AdminTranslationCodeId, LanguageId, Text)
VALUES
(NEWID(), 'BUTTON_Accept', 'it', 'Accetta'),
(NEWID(), 'BUTTON_Accept', 'en', 'Accept'),
(NEWID(), 'BUTTON_Cancel', 'it', 'Annulla'),
(NEWID(), 'BUTTON_Cancel', 'en', 'Cancel'),
(NEWID(), 'LABEL_Actions', 'it', 'Azioni'),
(NEWID(), 'LABEL_Actions', 'en', 'Actions'),
(NEWID(), 'LABEL_Column', 'it', 'Colonna');
(NEWID(), 'LABEL_Column', 'en', 'Column'),
(NEWID(), 'MESSAGE_Confirm', 'it', 'Conferma'),
(NEWID(), 'MESSAGE_Confirm', 'en', 'Confirm');
(NEWID(), 'MESSAGE_GoBack', 'it', 'Torna indietro'),
(NEWID(), 'MESSAGE_GoBack', 'en', 'Go back'),
GO
The Vim command: :/VALUES$/+,/^GO$/-2s/;$/,/ | /^GO$/-s/,$/;/. No need to comment on it; it’s barely
impossible to tell what it does in a quick glance; we have to decode it, and none of it will be executed
before you actually run the whole thing. It does a lot of things, and you can see a bunch of text that has to
be typed there, like VALUES GO, negative relative lines, etc.
The Kakoune version here is different, and, as expected, more interactive. The first thing we want to
do, as always, is to get a cursor on the lines starting with a NEWID, and put each cursor at the end of each
line. We still use % to select the whole buffer, sNEW<ret> to select all NEW, and gl to move all
selections on the last character, which will be a , or a ;, depending on the line:


Then, simply type r, to replace all ending symbols with ,:

We now need to clean up the last line, since it requires a ;. We have a couple solutions, that I will
explain.
I like this one a lot because it’s very interactive and intuitive. Kakoune has the concept of a primary selection and secondary selections. The primary selection is treated specifically from others, as it’s used to perform some specific actions. For instance, imagine you select a function and all of its occurrences. Depending on where your primary selection is, the context action you might get from e.g. LSP will be different.
Kakoune allows you to cycle the primary selection with secondary ones with ( and ). Usually, highlighting
will tell which selection is the primary — in my screenshot above, the primary is pink-like; the secondary
ones are white-like. If you have typed the keys as mentioned below, you should notice
that the primary selection is actually on the last line, which is already perfect! If it’s not, you can use
( and/or ) to adjust it; we want the last line to have the primary selection.
Then, just press ,. That key will drop all selections but the primary one, leaving a single selection on the
last ,. You then know what to do: r;. And we’re done!


Another thing that I use from time to time is to filter (keep) or discard (keep non-matching) selections. This
is useful to filter selections based off the already selected, disjoint selections. We use the <a-k> key
chord for that. In our case, we can see that the last line has the word back in it, which is unique to our
selections. We can select whole lines with x:

And then <a-k>back<ret> to select that line:

Note:
<a-k>(keep matching) is different froms(select) in the sense that it will filter selections, whileswill select whatever you type, discarding already existing selections. Try them out to experiment!
Then simply do the previous thing: glr;.
Something that I think is important to notice is that, in most cases, you will most likely just move your cursor there and modify the character manually. Sometimes, it’s much faster to drop all selections and manually edit a bunch of characters instead of trying to be smart and spending mental effort for no real benefit.
For this reason, I think the first option is clearly the way-to-go solution: just press ,, and the cursor
will already be at the right place. Kakouine encourages visual exploration and interactivity; stop
thinking too much about how to edit stuff in advance, and do it gradually, incrementally!
This one is absolutely crazy in Vim. Based on this text:
{{#if UserDetails.FirstName}}Nome: {{UserDetails.FirstName}}{{/if}}
{{#if UserDetails.LastName}}Cognome: {{UserDetails.LastName}}{{/if}}
{{#if CallbackDetails.CallbackTimeStamp}}Data e orario indicati dal cliente per la chiamata: {{dateFormat CallbackDetails.CallbackTimeStamp format="d"}}.{{dateFormat CallbackDetails.CallbackTimeStamp format="M"}}.{{dateFormat CallbackDetails.CallbackTimeStamp format="Y"}} {{dateFormat CallbackDetails.CallbackTimeStamp format="H"}}:{{dateFormat CallbackDetails.CallbackTimeStamp format="m"}}{{/if}}
{{#if UserMotivation.ReasonSell}}Motivo della vendita: {{UserMotivation.ReasonSell}}{{/if}}
Informazioni sull'immobile:
{{#if PropertyDetails.ObjectType}}Categoria: {{PropertyDetails.ObjectType}}{{/if}}
{{#if PropertyDetails.ObjectSubType}}Tipo di oggetto: {{PropertyDetails.ObjectSubType}}{{/if}}
{{#if PropertyDetails.Street}}Via: {{PropertyDetails.Street}} {{#if PropertyDetails.Number}}{{PropertyDetails.Number}}{{/if}}{{/if}}
{{#if PropertyDetails.Zipcode}}Luogo: {{PropertyDetails.Zipcode}}{{/if}} {{#if PropertyDetails.City}}{{PropertyDetails.City}}{{/if}}
Valutazione immobiliare:
{{#if ValuationDetails.EstimatedMarketValue}}Prezzo di mercato stimato: {{ValuationDetails.EstimatedMarketValue}}{{/if}}
{{#if ValuationDetails.MinimumPrice}}Prezzo minimo: {{ValuationDetails.MinimumPrice}}{{/if}}
{{#if ValuationDetails.MaximumPrice}}Prezzo massimo: {{ValuationDetails.MaximumPrice}}{{/if}}
We want to extract all field names from that Handlebars template snippet. To do that in Vim, you need to store the names in a register, remove duplicate lines, and format the result. Let’s see how to do that in Kakoune.
We start as always by selecting the whole buffer and follow a similar idea as in Vim by selecting the if,
but we will use the # as well — I think it’s more correct than using the trailing whitespace: %s#if<ret>:

That gives us a selection on each #if. We can then press w twice to fully select all field names:

Yank them with y, and drop your selections with ,. As with Vim, move your cursor in an empty location, and
paste your selections with <a-p>.

We cannot use
p, becauseppastes with a 1:1 mapping between selections. What it means is thatpis selection-aware, so if you have two selections containingfooandbar, yank them, then move both selections to somewhere else and pressp,foowill be pasted to wherever the first selection is, andbarwill be pasted on the other place. This is incredibly powerful, but here, since we have dropped all selections, pasting withpwill only paste the content of the primary selection.<a-p>will, as the doc says, paste all the selections one after the other, and will put a selection after each of them, which is exactly what we want.
Once pasted, you will notice they are all on the same line. Simply press i<ret><esc>x to put them ont
different lines and ensure you have a training newline on each of them:

In order to sort them, we need to end up with a single selection containing them all. We could do this by
dropping all selections with ,, then selecting the whole paragraph with <a-i>p, but Kakoune has a key for
this exact situation: <a-_>, which merges continuous selections, across lines:

Then simply press |sort -u<ret> to sort them via the sort program:

Now, you can get a selection back on each line by pressing <a-s>, which simply adds a cursor at the end of
each line in the current selections — note the plural; if you had several disjoint selections all spanning
on several lines, it would apply this logic to all of them, which is really powerful:

Press ; to reduce all selections to a single character (anchor = cursor):

And then simply go with r, to format the whole thing as a single comma-separated line:

You can drop selections to keep the primary (last one) and use the same trick as before to remove the
trailing ,: ,d.
It might sound like a lot is going on, but it actually is. The great thing is that everything is interactive, and you see what you are doing. Meanwhile, most of that in Vim occurs without you noticing everything, requiring to type a lot of normal commands and crazy substitutions. A single selection mistake will require you to go back from the beginning.
This one is pretty fun too. Given a git status output, we want to execute a git command on each line.
Note: I would have actually run a single
git update-indexcommand with all the paths as arguments instead, but that defeats the exercise, so let’s stick to just calling the git command for each path instead.
The output is:
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: ExcelGenerator/App.Debug.config
modified: ExcelGenerator/App.config
modified: Core/ConfigBase.cs
modified: Infrastructure/Security/Password.cs
modified: Tools.AgentExport/App.Debug.config
modified: Tools.AgentExport/App.config
modified: Tools.CheckLeadsForConfirmedEmail/App.Debug.config
modified: Tools.CheckLeadsForConfirmedEmail/App.config
modified: Tools.EmailSender/App.Debug.config
modified: Tools.EmailSender/App.config
modified: Tools.LeadChecker/App.Debug.config
modified: Tools.LeadChecker/App.config
modified: Tools.LeadImport/App.Debug.config
modified: Tools.LeadImport/App.config
modified: Tools.LeadManager/App.Debug.config
modified: Tools.LeadManager/App.config
modified: Tools.LeadReminder/App.Debug.config
modified: Tools.LeadReminder/App.config
modified: Tools.SmsSender/App.Debug.config
modified: Tools.SmsSender/App.config
modified: Web.Backend/Web.Backend.csproj
modified: Web.Backend/Web.Debug.config
modified: Web.Backend/Web.config
In Vim, we need to run a bunch of commands: bring the output of git status in the buffer, remove lines
not containing a modified path, map all lines to git commands, and execute all lines as bash commands.
Let’s see how I would do that in Kakoune.
First, as a disclaimer, I need to mention that Kakoune has a support for git via its standard distribution
and the :git command. It would be cheating, so I will not use it.
From an empty buffer, let’s bring in the output of git status by simply typing !git status — let’s change
the output to match whatever the exercise has:

Then, it’s pretty simple: %smod<ret>wwlGl to select all paths:

And it’s almost done. Press <a-|> to pipe each selection as standard input of the program of your choice,
and use xargs git update-index --skip-worktree <ret>. This will run that program with each selection as
argument.
Another crazy one from Vim: from a text dump of some random data (IP, masks, subnets), we want to create some C# code that will whitelist those IP addresses. The goal is to format around the data, and it’s really bad for Vim, because its regex thing is absolutely disgusting, while Kakoune was designed specifically with that kind of usecase in mind.
Just so you see it, the Vim way requires to format the data, extract texts in many registers, map the data to actual code, and other things, which results in those lines:
`[v`]:'<,'>s/^.\(\d*\).\{-}\(255.*\)\t.*/'\1': '\2',/ | '<,'>j | s/.*/{ & }/
/new Re<Enter>"ayf";;"byf";;"cy$
`[v`]:'<,'>g/^$/d
gv:'<,'>s/\(.*\)\/\(.*\)$/\='<C-r>a' . submatch(1) . '<C-r>b' . <C-r>m[submatch(2)] . '<C-r>c'
Now let’s see how to do that in Kakoune. We start with the same buffer as Vim:

I think the best approach here is to use a macro — yes! Kakoune has them! — and a wonderful feature of Kakoune regarding selections: executing arbitrary keys on each of the selection. First, let’s paste one IP address:

We can start the macro by pressing the Q key — the macro will be registered to the @ register by default.
Start by moving at the end of the current line with gl:

Then, we are going to do a reverse character search, yank the subnet mask index, and put a mark to save
the current selection with <a-t>/yZ:

Zhere is a feature that is not available on Helix; you can probably use the jump list’s<c-s>feature in Helix there, but I haven’t tried.
Now, the default " register has the mask index. We can just do a regular search of /N with N the mask
index with the //<c-r>"\t<ret> keys:

We can move the cursor to the right to select the actual mask value with wwl?\tH:

?is the extend version of/. We need to use this because the values are separated by\t. If it was a regular space, we could have usedtinstead.
Now, let’s yank that value in the m register to be sure we won’t lose it with "my:

Press z. It will restore your selection back to the subnet index in the IP address:

From this place, we can start formatting the line. Let’s copy the IP address into the i register, and remove
the rest. Go to the beginning of the line with gi, select up to the / with t/, and "iy to yank the IP
address into the i register:

Replace the whole line with x_:

Press r and start writing your code. Whenever you need to put the IP address, use <c-r>i. To put the mask,
use <c-r>m:

And now that this is done, finish recording with Q. Now, for every more lines we have an IP address on, just
press q to replace it with the code:

However, there is a better feature of Kakoune we can use here.
execute-keysexecute-keys — which we often use abbreviated exec — allows to type keys as if they were typed by the user
in a more controlled way. For instance, this command accepts the -itersel flag, which runs the command for
every selection. Along with the -with-maps (required for macros, I think?), we can select all IP addresses,
spawn a selection on each of them with <a-s> as seen previously:

And then press :exec -itersel -with-maps q:

Press <ret>. Taaadaaa:

The design of Kakoune shines here. You can see that you can do everything by incrementally and visually
progress through your mental model, instead of having to think of everything in advance. The fact that you can
use regular commands such as w, y, d, f etc. on many selections at once creates such a friendly
environment to implement very complex workflows, without needing to use the crazy regex and substitute
commands from Vim.
I hope that blog article provided a good hindsight on how we can do things in Kakoune vs. Vim, and why many people — me included — prefer the modal editing model of Kakoune.
Keep the vibes!
]]>This is a very short article to make you notice that al received two important changes:
I tested the latter with a Windows 64b:
cabal update
cabal install al
That’s all. You should try it out as well. If you have errors about OpenAL libraries and/or header files, check whether OpenAL is correctly installed. If the error comes up again, proceed as I said here.
Also, I take people on linux feedback about installs ;).
]]>Imagine you have a crate A and a crate B and you want to upload them to crates.io. There is a
context setup:
A depends on B via [dev-dependencies] because you use it in your examples/ directory,
for instance.B depends on A via [dependencies] because it implements some interface or such.A first (because uploading B without having A would make B
unable to compile).The main problem is that currently, you cannot do anything with such a setup. If you try to
upload A to crates.io, crates.io will reject the crate because B doesn’t exist. That might be
stunning, but yes, cargo also check that your [dev-dependencies] are correctly aligned with your
A’s Cargo.toml. Meh.
If you try to upload B, obviously, since A is not there, crates.io will complain.
What should we do?
First thing first, here’s a discussion that helped me figure out how to solve that issue. Disclaimer, though: it’s neither elegant and satisfying.
The first thing to do is to edit A’s Cargo.toml in order to completely strip its
[dev-dependencies] section. Just remove it. All of it.
Then, and that’s the tricky part: do not git commit the change. At that point, you have removed
the dependency from A to B, which is not mandatory for people getting the crate from
crates.io. That’s the nasty part. You’re about to upload a crate which metadata are not aligned
with your repository. I don’t like it, but I don’t see any other way to do it — however, you’ll see
we can fix it afterwards.
Then, upload A to crates.io:
cd A
cargo publish --allow-dirty
The --allow-dirty switch is needed because cargo would complain about your working tree not
being clean otherwise.
Now, you have A on crates.io. Just go on with B: it can now be uploaded:
cd ../B
cargo publish
Now you have, on crates.io:
A in version X.Y.Z, without any [dev-dependencies].B, whichever version.The final part of the trick is to restore the sanity of A. Because, yes, I don’t like having
something on crates.io that is not exactly the same thing as my repository.
Edit A’s Cargo.toml or check it out:
cd ..
git checkout .
Now you are exactly in the same situation as before trying anything to solve the problem. Edit
A’s Cargo.toml and increment its patch version. In my case, I would get version X.Y.(Z+1).
git commit your changes, git push. You’re ready to patch crates.io:
cd A
cargo publish
Now you have:
A in version X.Y.(Z+1), with the right [dev-dependencies].B, whichever version.I know it’s not an ideal fix, but at least we end up on our (clean) feets. I really really think
the cargo team should fix that issue, though. In that case, A and B were actually luminance
and luminance-derive. I will make a long article very soon about them to introduce new versions,
new crates and complete graphics tutorials on how to have fun with luminance.
I hope you liked that article and as always, keep the vibes.
]]>This will be a short announcement about al, a Haskell wrapper to the OpenAL C library.
Currently, the wrapper has been successfully tested on Linux – at least it works well on my Archlinux distro. I made a little program that reads from an .ogg file and streams the PCM signal to the wrapper – see libvorbis for further details. I’ll release the program later on if I find the spare time.
The wrapper might also work on Windows as long as you have pkg-config installed. I’d be very happy with feedback from people working on Windows. I don’t want anyone be put apart with my packages.
However, I’ve never tested the wrapper on Mac OS X. I guessed it wouldn’t work
out of the box because Mac OS X doesn’t use regular libraries to compile and
link – that would have been too easy otherwise, and hell, think different
right? They use something called a framework.
It’s possible to include a framework in a Haskell project by fulfilling
the frameworks field in the .cabal file. I received a simple patch to do
that – here, and I merged it upstream.
Then, Mac OS X is now officially supported. The release version is the 0.1.4.
There’s something else I’d like to discuss. Quickly after the first release of
al, I decided to push it onto
stackage. Unfortunately, there’s a problem with the
package and Ubuntu. For a very dark reason, Ubuntu doesn’t expose
anything when invoking pkg-confg --cflags, even if the files are there – on
Ubuntu they can be found in /usr/include/AL.
That’s very silly because I don’t want to hardcode the location – it might be
something else on other Linux distro. The problem might be related to the
OpenAL support in Ubuntu – the .pc file used by pkg-config might be
incomplete. So if you experience that kind of issue, you can fix it by passing
the path to your OpenAL headers:
cabal install al --extra-include-dirs=/usr/include/AL
If OpenAL is installed somewhere else, consider using:
find / -name al.h
I’ll do my best to quickly fix that issue.
]]>From luminance-0.1 to luminance-0.2 included, it was not possible to use any texture types different than two-dimensional textures. This blog entry tags the new release, luminance-0.3, which adds support for several kinds of texture.
Texture1D,
Texture2D and
Texture3D
are all part of the new release. The interface has changed – hence the breaking changes yield a
major version increment – and I’ll explain how it has.
Basically, textures are now fully polymorphic and are constrained by a typeclass:
Texture.
That typeclass enables ad hoc polymorphism. It is then possible to add more texture types without
having to change the interface, which is cool. Like everything else in luminance, you just have to
ask the typesystem which kind of texture you want, and everything will be taken care of for you.
Basically, you have three functions to know:
createTexture,
which is used to create a new texture ;uploadSub,
used to upload texels to a subpart of the texture ;fillSub,
used to fill – clear – a subpart of the texture with a given value.All those functions work on (Texture t) => t, so it will work with all kinds of texture.
Cubemaps
are also included. They work like other textures but add the concept of faces. Feel free to dig in
the documentation for further details.
I need to find a way to wrap texture arrays, which are very nice and useful for layered rendering. After that, I’ll try to expose the change to the framebuffers so that we can create framebuffers with cubemaps or that kind of cool feature.
In the waiting, have a good a week!
]]>This blog entry directly follows the one in which I introduced Ash, a shading language embedded in Haskell. Feel free to read it here before going on.
A shader is commonly a function. However, it’s a bit more than a simple function. If you’re a haskeller, you might already know the MonadReader typeclass or simply Reader (or its transformer version ReaderT). Well, a shader is kind of a function in a reader monad.
So… that implies a shader runs in… an environment?
Yeah, exactly! And you define that environment. The environment is guaranteed not to change between two invocations of a shader for the same render (e.g. between two vertices in the same render). This is interesting, because it enables you to use nice variables, such as time, screen resolution, matrices and whatever your imagination brings on ;)
The environment, however, can be changed between two renders, so that you can update the time value passed to the shader, the new resolution if the window resizes, the updated matrices since your camera’s moved, and so on and so forth.
Let’s see a few example in GLSL first.
To control the environment of a shader in GLSL, we use uniform variables. Those are special, global variables and shared between all stages of a shader chain1.
Let’s see how to introduce a few uniforms in our shader:
uniform float time; // time of the host application
uniform vec2 resolution; // (w,h)
uniform vec2 iresolution; // (1 / w, 1 / h), really useful in a lot of cases ;)
uniform mat4 proj; // projection matrix
uniform int seed; // a seed for whatever purpose (perlin noise?)
uniform ivec2 gridSize; // size of a perlin noise grid!
You got it. Nothing fancy. Those uniforms are shared between all stages so that we can use time in all our shaders, which is pretty cool. You use them as any kind of other variables.
Ok, let’s write an expression that takes a time, a bias value, and multiply them between each other:
uniform float time;
uniform float bias;
// this is not a valid shader, just the expression using it
time * bias;
HLSL uses the term constant buffers to introduce the concept of environment. I don’t have any examples right now, sorry for the inconvenience.
In Ash, environment variables are not called uniforms nor constant buffers. They’re called… CPU variables. That might be weird at first, but let’s think of it. Those values are handled through your application, which lives CPU-side. The environment is like a bridge between the CPU world and the GPU one. A CPU variable refers to a constant value GPU-side, but varying CPU-side.
Create a CPU variable is pretty straight-forward. You have to use a function called cpu. That function is a monadic function working in the EDSL monad. I won’t describe that type since it’s still a work in progress, but it’s a monad for sure.
Note: If you’ve read the previous blog entry, you might have come across the
Ashtype, describing a HOAST. That type is no more a HOAST. The “new Ash” – the type describing the HOAST – is now Expr.
This is cpu:
cpu :: (CPU a) => Chain (Expr a)
CPU is a typeclass that enables a type to be injected in the environment of a shader chain. The instances are provided by Ash and you can’t make your own – do you really want to make instance CPU String, or instance (CPU a) => CPU (Maybe a)? Don’t think so ;)
Let’s implement the same time–bias example as the GLSL one:
foo :: Chain (Expr Float)
foo = liftA2 (*) cpu cpu
That example is ridiculous, since in normal code, you’d actually want to pass the CPU variables to nested expressions, in shaders. So you could do that:
foo :: Chain ()
foo = do
time <- cpu
bias <- cpu
-- do whatever you want with time and bias
return ()
Chain is a new type I introduce in this paper. The idea came up from a discussion I had with Edward Kmett when I discovered that the EDSL needed a way to bind the CPU variables. I spotted two ways to go:
String, passed to cpu; that would result in writing the name in every shader using it, so that’s not ideal;The latter also provides a nice hidden feature. A chain of shaders might imply varying2 values. Those varying values have information attached. If you mistake them, that results in catastrophic situations. Using a higher monadic type to capture that information – along with the environment – is in my opinion pretty great because it can prevent you from going into the wall ;).
To sum up, Chain provides a clear way to describe the relation between shaders.
I’m still building and enhancing Ash. In the next post, I’ll try to detail the interface to build functions, but I still need to find how to represent them the best possible way.
You can imagine a shader chain as an explicit composition of functions (i.e. shaders). For instance, a vertex shader followed by geometry shader, itself followed by a fragment shader.
Varying values are values that travel between shaders. When a shader outputs a value, it can go to the input of another shader. That is call a varying value.
Just to let you fully understand the problem, let me introduce a few principles from alto. As a wrapper over OpenAL, it exposes quite the same interface, but adds several safe-related types. In order to use the API, you need three objects:
Alto object, which represents the API object (it holds dynamic library handles, function pointers, etc. ; we don’t need to know about that)Device object, a regular device (a sound card, for example)Context object, used to create audio resources, handle the audio context, etc.There are well-defined relationships between those objects that state about their lifetimes. An Alto object must outlive the Device and the Device must outlive the Context.
Basically:
let alto = Alto::load_default(None).unwrap(); // bring the default OpenAL implementation in
let dev = alto.open(None).unwrap(); // open the default device
let ctx = dev.new_context(None).unwrap(); // create a default context with no OpenAL extension
As you can see here, the lifetimes are not violated, because alto outlives dev which outlives ctx. Let’s dig in the type and function signatures to get the lifetimes right
(documentation here).
fn Alto::open<'s, S: Into<Option<&'s CStr>>>(&self, spec: S) -> AltoResult<Device>
The S type is just a convenient type to select a specific implementation. We need the default one, so just pass None. However, have a look at the result. AltoResult<Device>. I told you about
lifetime relationships. This one might be tricky, but you always have to wonder “is there an elided lifetime here?”. Look at the Device type:
pub struct Device<'a> { /* fields omitted */ }
Yep! So, what’s the lifetime of the Device in AltoResult<Device>? Well, that’s simple: the lifetime elision rule in action is one of the simplest:
If there are multiple input lifetime positions, but one of them is &self or &mut self, the lifetime of self is assigned to all elided output lifetimes. (source)
So let’s rewrite the Alto::open function to make it clearer:
fn Alto::open<'a, 's, S: Into<Option<&'s CStr>>>(&'a self, spec: S) -> AltoResult<Device<'a>> // exact same thing as above
So, what you can see here is that the Device must be valid for the same lifetime as the reference we pass in. Which means that Device cannot outlive the reference. Hence, it cannot outlive the
Alto object.
impl<'a> Device<'a> {
// …
fn new_context<A: Into<Option<ContextAttrs>>>(&self, attrs: A) -> AltoResult<Context>
// …
}
That looks a bit similar. Let’s have a look at Context:
pub struct Context<'d> { /* fields omitted */ }
Yep, same thing! Let’s rewrite the whole thing:
impl<'a> Device<'a> {
// …
fn new_context<'b, A: Into<Option<ContextAttrs>>>(&'b self, attrs: A) -> AltoResult<Context<'b>>
// …
}
Plus, keep in mind that self is actually Device<'a>. The first argument of this function then awaits a &'b Device<'a> object!
rustc is smart enough to automatically insert the
'a: 'blifetime bound here – i.e. the ’a lifetime outlives ’b. Which makes sense: the reference will die before theDevice<'a>is dropped.
Ok, ok. So, what’s the problem then?!
The snippet of code above about how to create the three objects is straight-forward (though we don’t take into account errors, but that’s another topic). However, in my demoscene framework, I really don’t want people to use that kind of types. The framework should be completely agnostic about which technology or API is used internally. For my purposes, I just need a single type with a few methods to work with.
Something like that:
struct Audio = {}
impl Audio {
pub fn new<P>(track_path: P) -> Result<Self> where P: AsRef<Path> {}
pub fn toggle(&mut self) -> bool {}
pub fn playback_cursor(&self) -> f32 {}
pub fn set_playback_cursor(&self, t: f32) {}
}
impl Drop for Audio {
fn drop(&mut self) {
// stop the music if playing; do additional audio cleanup
}
}
This is a very simple interface, yet I don’t need more. Audio::set_playback_cursor is cool when I debug my demos in realtime by clicking a time panel to quickly jump to a part of the music.
Audio::toggle() enables me to pause the demo to inspect an effect in the demo. Etc.
However, how can I implement Audio::new?
The problem kicks in as we need to wrap the three types – Alto, Device and Context – as the fields of Audio:
struct Audio<'a> {
alto: Alto,
dev: Device<'a>,
context: Context<'a>
}
We have a problem if we do this. Even though the type is correct, we cannot correctly implement Audio::new. Let’s try:
impl<'a> Audio<'a> {
pub fn new<P>(_: P) -> Result<Self> where P: AsRef<Path> {
let alto = Alto::load_default(None).unwrap();
let dev = alto.open(None).unwrap();
let ctx = dev.new_context(None).unwrap();
Ok(Audio {
alto: alto,
dev: dev,
ctx: ctx
})
}
}
As you can see, that cannot work:
error: `alto` does not live long enough
--> /tmp/alto/src/main.rs:14:15
|
14 | let dev = alto.open(None).unwrap();
| ^^^^ does not live long enough
...
22 | }
| - borrowed value only lives until here
|
note: borrowed value must be valid for the lifetime 'a as defined on the body at 12:19...
--> /tmp/alto/src/main.rs:12:20
|
12 | fn new() -> Self {
| ^
error: `dev` does not live long enough
--> /tmp/alto/src/main.rs:15:15
|
15 | let ctx = dev.new_context(None).unwrap();
| ^^^ does not live long enough
...
22 | }
| - borrowed value only lives until here
|
note: borrowed value must be valid for the lifetime 'a as defined on the body at 12:19...
--> /tmp/alto/src/main.rs:12:20
|
12 | fn new() -> Self {
| ^
error: aborting due to 2 previous errors
What’s going on here? Well, we’re hitting a problem called the problem of self-borrowing. Look at the first two lines of our implementation of Audio::new:
let alto = Alto::load_default(None).unwrap();
let dev = alto.open(None).unwrap();
As you can see, the call to Alto::open borrows alto – via a &Alto reference. And of course, you cannot move a value that is borrowed – that would invalidate all the references pointing to it.
We also have another problem: imagine we could do that. All those types implement Drop. Because they basically all have the same lifetime, there’s no way to know which one borrows information from whom.
The dropchecker has no way to know that. It will then refuse to code creating objects of this type, because dropping might be unsafe in that case.
Currently, this problem is linked to the fact that the lifetime system is a bit too restrictive and doesn’t allow for self-borrowing. Plus, you also have the dropchecker issue to figure out. Even
though we were able to bring in alto and device altogether, how do you handle context? The dropchecker doesn’t know which one must be dropped first – there’s no obvious link at this stage between
alto and all the others anymore, because that link was made with a reference to alto that died – we’re moving out of the scope of the Audio::new function.

That’s a bit tough. The current solution I implemented to fix the issue is ok–ish, but I dislike it because it adds a significant performance overhead: I just moved the initialization code in a thread that
stays awake until the Audio object dies, and I use a synchronized channel to communicate with the objects in that thread. That works because the thread provides us with a stack, that is the support of
lifetimes – think of scopes.
Another solution would be to move that initialization code in a function that would accept a closure – your application. Once everything is initialized, the closure is called with a few callbacks to
toggle / set the cursor of the object living “behind” on the stack. I don’t like that solution because it modifies the main design – having an Audio object was the goal.
Other solutions are:
std::mem::transmute to remove the lifetimes (replace them with 'static). That’s hyper dangerous and we are just breaking Rust’s lifetimes… not okay :(I don’t have a satisfying solution yet to that problem. My thread solution works and lets me have a single type abstracting all of that, but having a thread for such a thing is a waste of resources to me. I
think I’ll implement the closure solution as, currently, it’s not possible to embed in struct lifetimes’ semantics / logic. I guess it’s okay; I guess the problem is also linked to the fact the concept is
pretty young and we’re still kind of experimenting it. But clearly, lifetimes hit a hard problem here that they cannot solve correctly. Keep in mind that even if unsafe solutions exist, we’re talking about
a library that’s designed to work with Rust lifetimes as a pretty high level of abstraction. Firing transmute is very symptomatic of something wrong. I’m open to suggestions, because I’ve been thinking the
problem all day long without finding a proper solution.
Keep the vibe!
]]>So, as always, once a year, I attend Revision. But this year, it was a bit different for me. Revision is very impressive and most of the “big demogroups” release their productions they’ve been working on for months or even years. I tend to think “If I release something here, I’ll just be kind of muted by all those massive productions.” Well, less than two weeks before Revision 2017, I was contacted by another demogroup. They asked me to write an invitro – a kind of intro or demo acting as a communication production to invite people to go to another party. In my case, I was proposed to make the Outline 2017 invitro. Ouline was the first party I attended years back then, so I immediately accepted and started to work on something. That was something like 12 days before the Revision deadline.
I have to tell. It was a challenge. All productions I wrote before was made in about a month and a half and featured less content than the Outline Invitation. I had to write a lot of code from scratch. A lot. But it was also a very nice project to test my demoscene framework, written in Rust – you can find spectra here for now; it’s unreleased by the time I’m writing this but I plan to push it onto crates.io very soon.
An hour before hitting the deadline, the beam team told me their Ubuntu compo machine died and that it would be neat if I could port the demo to Windows. I rushed like a fool to make a port – I even forked and modified my OpenAL dependency! – and I did it in 35 minutes. I’m still a bit surprised yet proud!
Anyways, this post is not about bragging. It’s about hindsight. I did that for Céleri Rémoulade as I was the only one working on it – music, gfx, direction and obviously the Rust code. I want to draw a list of what went wrong and what went right. In the first time, for me. So that I have enhancement axis for the next set of demos I’ll do. And for sharing those thoughts so that people can have a sneak peek into the internals of what I do mostly – I do a lot of things! :D – as a hobby on my spare time.
Sooooooooo… What went wrong. Well, a lot of things! spectra was designed to build demo productions in the first place, and it’s pretty good at it. But something that I have to enhance is the user interaction. Here’s a list of what went wrong in a concrete way.
With that version of spectra, I added the possibility to hot-reload almost everything I use as a resource: shaders, textures, meshes, objects, cameras, animation splines, etc. I edit the file, and as soon as I save it, it gets hot-reloaded in realtime, without having to interact with the program (for curious ones, I use the straight-forward notify crate for registering callbacks to handle file system changes). This is very great and I save a lot of time – Rust compilation is slow, and that’s a lesson I had learned from Céleri Rémoulade: keeping closing the program, make a change, compiling, running is a wast of time.
So what’s the issue with that? Well, the main problem is the fact that in order to implement
hot-reloading, I wanted performance and something very simple. So I decided to use shared mutable
smart states. As a Haskeller, I kind of offended myself there – laughter! Yeah, in the
Haskell world, we try hard to avoid using shared states – IORef – because it’s not referentially
transparent and reasoning about it is difficult. However, I tend to strongly think that in some very
specific cases, you need such side-effects. I’m balanced but I think it’s the way to go.
Anyway, in Rust, shared mutable state is implemented via two types: Rc/Arc and Cell/RefCell.
The former is a runtime implementation of the borrowing rules and enables you to share a pointer. The borrowing rules are not enforced at compile-time anymore but dynamically checked. It’s great because in some time, you can’t know how long your values will be borrowed or live. It’s also dangerous because you have to pay extra attention to how you borrow your data – since it’s checked at runtime, you can crash your program if you’re not extra careful.
Rcmeans ref counted andArcmeans atomic-ref counted. The former is for values that stay on the same and single thread; the latter is for sharing the pointer between threads.
Cell/RefCell are very interesting types that provide internal mutation. By default, Rust gives
you external mutation. That is, if you have a value and its address, can mutate what you have at
that address. On the other hand, internal mutation is introduced by the Cell and RefCell
types. Those types enable you to mutate the content of an object stored at a given address without
having the exterior mutatation property. It’s a bit technical and related to Rust, but it’s often
used to mutate the content of a value via a function taking an immutable value.
Cellonly accepts values that can be copied bit-wise andRefCellworks with references.
Now, if you combine both – Rc<RefCell<_>>, you end up with a shareable – Rc<_> – mutable –
RefCell<_> – value. If you have a value of type Rc<RefCell<u32>> for instance, that means you
can clone that value and store it everywhere in the same thread, and at any time, borrow it and
inspect and/or mutate its content. All copies of the values will observe the change. It’s a bit like
C++’s shared_ptr, but it’s safer – thank you Rust!
So what went wrong with that? Well, the borrow part. Because Rust is about safety, you still need to
tell it how you want to borrow at runtime. This is done with the [RefCell::borrow()](https://doc.rust-lang.org/std/cell/struct.RefCell.html#method.borrow]
and RefCell::borrow_mut()
functions. Those functions return a special object that borrows the pointed object as long as it
lives. Then, when it goes out of scope, it releases the borrow.
So any time you want to use an object that is hot-reloadable with my framework, you have to call one of the borrow functions presented above. You end up with a lot of borrows, and you have to keep in mind that you can litterally crash your program if you violate the borrowing rules. This is a nasty issue. So far, I haven’t really spent time trying to fix that, but that something I have to figure out.
This is a bit tricky. As a programmer, I’m used to write algorithms and smart architectures to transform data and resolve problems. I’m given inputs and I provide the outputs – the solutions. However, a demoscene production is special: you don’t have inputs. You create artsy audiovisual outputs from nothing but time. So you don’t really write code to solve a problem. You write code to create something that will be shown on screen or in a headphone. This aspect of demo coding has an impact on the style and the way you code. Especially in crunchtime. I have to say, I was pretty bad on that part with that demo. To me, code should only be about transformations – that’s why I love Haskell so much. But my code is clearly not.
If you know the let keyword in Rust, well, imagine hundreds and hundreds of lines starting with
let in a single function. That’s most of my demo. In rush time, I had to declare a lot of things
so that I can use them and transform them. I’m not really happy with that, because those were data
only. Something like:
let outline_emblem = cache.get::<Model>("Outline-Logo-final.obj", ()).unwrap();
let hedra_01 = cache.get::<Model>("DSR_OTLINV_Hedra01_Hi.obj", ()).unwrap();
let hedra_02 = cache.get::<Model>("DSR_OTLINV_Hedra02_Hi.obj", ()).unwrap();
let hedra_04 = cache.get::<Model>("DSR_OTLINV_Hedra04_Hi.obj", ()).unwrap();
let hedra_04b = cache.get::<Model>("DSR_OTLINV_Hedra04b_Hi.obj", ()).unwrap();
let twist_01 = cache.get::<Object>("DSR_OTLINV_Twist01_Hi.json", ()).unwrap();
It’s not that bad. As you can see, spectra features a resource cache that provides several candies – hot-reloading, resource dependency resolving and resource caching. However, having to declare those resources directly in the code is a nasty boilerplate to me. If you want to add a new object in the demo, you have to turn it off, add the Rust line, re-compile the whole thing, then run it once again. It breaks the advantage of having hot-reloading and it pollutes the rest of the code, making it harder to spot the actual transformations going on.
This is even worse with the way I handle texts. It’s all &'static str declared in a specific file
called script.rs with the same insane load of let. Then I rasterize them in a function and use
them in a very specific way regarding the time they appear. Not fun.
Most of the varying things you can see in my demos are driven by animation curves – splines. The bare concept is very interesting: an animation contains control points that you know have a specific value at a given time. Values in between are interpolated using an interpolation mode that can change at each control points if needed. So, I use splines to animate pretty much everything: camera movements, objects rotations, color masking, flickering, fade in / fade out effects, etc.
Because I wanted to be able to edit the animation in a comfy way – lesson learned from Céleri Rémoulade, splines can be edited in realtime because they’re hot-reloadable. They live in JSON files so that you just have to edit the JSON objects in each file and as soon as you save it, the animation changes. I have to say, this was very ace to do. I’m so happy having coded such a feature.
However, it’s JSON. It’s already a thing. Though, I hit a serious problem when editing the orientations data. In spectra, an orientation is encoded with a unit quaternion. This is a 4-floating number – hypercomplex. Editing those numbers in a plain JSON file is… challenging! I think I really need some kind of animation editor to edit the spline.
]]>The points expressed here are written by decreasing priority, starting with the feature I would like the most to see implemented as soon as possible.
It’s for sure one among the two features I miss the most from Haskell. Rank-N quantification is a feature that, when I discovered it almost 9 years ago, changed a lot of things in my way of thinking and desiging interfaces.
For those not used to it or those having no idea what rank-N quantification is, let me explain with simple words by taking an example.
Imagine a function that works on a vector of u32:
fn process(v: Vec<u32>)
That function is monomorphic. You cannot, at compile-time, get several flavours of process.
But now imagine you want to process lots of things without actually requiring the element type
to be u32 but, let’s say, T: Into<u32>:
fn process<T>(v: Vec<T>) where T: Into<u32>
That function has a type variable, T, and we say that is has a rank of 1, or it’s a rank-1
function. If you add more variables, they all remain at the same level, so the function is
still rank-1:
fn process<T, Q>(v: Vec<T>, other: Option<Q>) where T: Into<u32>, Q: Display
Now, imagine a function that would take as sole argument a function which takes a u32 and
returns a String, for instance:
fn foo<F>(f: F) where F: Fn(u32) -> String {
println!("the function returned {}", f(123));
}
That function is still rank-1. But now, imagine that we would like to express the idea that the
function that is passed as argument must work for any T: Debug instead of u32. Here, the
any word is important, because it means that you must pass a polymorphic function to foo, which
will get monomorphized inside the body of foo. If you’ve thought about this:
fn foo<F, T>(f: F) where F: Fn(T) -> String, T: Debug {
println!("the function returned {}", f(123u32));
}
Then you’re wrong, because that function cannot compile. The reason for this is that the type of f
is F: Fn(T) -> String and you try to pass 123u32 instead of T. That function definition cannot
work because the body would force T to be u32. The problem here is that, currently, Rust doesn’t
allow us to do what we want to: T shouldn’t be monomorphized at the same rank as F, because
T will be chosen by the implementation of foo, not the caller!
I would like this:
fn foo<F>(f: F) where F: for<T> Fn(T) -> String where T: Debug;
// or
fn foo<F>(f: F) where F: for<T: Debug> Fn(T) -> String;
We call that a rank-2 function, because it has two levels / ranks of type variables. We could use it this way:
fn foo<F>(f: F) where F: for<T> Fn(T) -> String where T: Debug {
println!("the function returned {}", f(123u32);
println!("the function returned {}", f("Hello, world!");
}
You can imagine rank-N quantification by nesting HRTB syntax:
// a rank-3 function
fn foo<F>(f: F) where F: for<T> Fn() -> T where T: for<X> Into<X> where X: Debug;
But it’s rarely needed and I struggle to find a real usecase for them (but there are!). From my Haskell experience, we really really rarely need more than rank-2 quantification.
You can find more in-details thoughts of that feature on a previous article of mine, here.
“Kinds” is the second feature I would love to see in Rust. For those who don’t know what they are, consider:
let x = 3,
3 is a value and x is a binding to that value.u32 is a type and in fn foo<T>(), T is a type variable.
It’s actually a free variable, here. Currently, Rust stops here.In Haskell but also Idris, ATS, Coq and many others, types have types too. We name those kinds. To understand what it means:
u32 is a label (really, imagine "u32") of a very big
set that contains all the possible values that can be labelled as u32. You find in that
set 0, 1, 34, 2390743, etc.For instance, imagine the kind Number. You can put in that set the types u32, i32, usize,
f32, etc. But now imagine: type variables are to types what variables are to values. What would
be a kind variable? Well, it would be something that would allow us to give more details about
what a type should be. For instance:
trait Functor {
fn map<A, B, F>(self: Self<A>, f: F) -> Self<B>;
}
impl Functor for Option {
fn map<A, B, F>(self: Self<A>, f: F) -> Self<B> {
self.map(f)
}
}
// whatever the type of functor, just ignore its content and replace with ()
// we could also introduce a type variable A and use fct: Fct<A> but since we don’t
// need it, we use _
//
// The <Fct<_>> marks the kind of Fct (i.e. its kind is Type -> Type, as in it expects
// a type to be type)
fn void<Fct<_>>(fct: Fct<_>) -> Fct<()> where Fct: Functor {
fct.map(|_| ())
}
Currently, that syntax doesn’t exist and I don’t even know how it would be formalized. The void
function above looks okay to me but not the trait definition. The syntax T<_, _> would
declare a type which must has two type variables, etc.
GATs are a bit akin to kinds in the sense that they
allow to express type constructors (i.e. which kinds are, for instance, Type -> Type, if they only
have one type variable).
That’s a feature I need a lot in several crates of mine, so I hope it will be implemented and stable soon! :)
Something I want a lot too and hasn’t been discussed a lot (I might write an RFC for that because I want it very badly). What it means is that:
let x = 3;
The type of x here would be polymorphic as T: FromLit<usize>. We would have several implementors
and it would go like this:
pub trait FromLit<L>: L: Lit {
const fn from_lit(lit: L) -> Self;
}
// blanket impl
impl<L> FromLit<L> where L: Lit {
const fn from_lit(lit: L) -> Self {
lit
}
}
// generated by rustc
impl Lit for usize {}
impl Lit for isize {}
impl Lit for u32 {}
impl Lit for &'static str {}
// …
This would allow us to do something like that:
pub enum Expr {
ConstBool(bool),
ConstI32(i32),
// …
}
impl FromLit<bool> for Expr {
const fn from_lit(lit: L) -> Self {
Expr::ConstBool(lit)
}
}
// in a function
let expr: Expr = false;
As a rationale, Haskell has that in its base language since forever and under the language extension
called OverloadedStrings and OverloadedLists for strings and lists.
A feature that wouldn’t make everyone happy, so I’m pretty sure it will not be in the Rust 2020 roadmap (and maybe never end up in Rust at all, sadly), but I think it’s worth mentioning it.
I would be able to create custom operators. The reason for this is simple: EDSLs. I love EDSLs. Having the possibility to enrich expressiveness via custom operators is something I’ve been wanting for quite a while now and I’m so surprised people haven’t arised that concern yet.
I know there is concerns from people who know the Haskell lens library and its infamous lists of horrible and awful operators, but that’s not a reason to block such a feature to me, for two reasons:
lens is really extreme is likely the sole real problem in the Haskell ecosystem.I’m a huge partisan of the idea that there are billions of people speaking Chinese, a language very cryptic to me, because I just cannot speak Chinese. Yet it doesn’t prevent billions of people speaking Chinese on a daily basis without any problem. It’s always a question of learning and an operator should be perceived as a function name. Stop spreading fear about readability: a convoluted function name is also pretty hard to read.
To mitigate fear even further, there are several very good operators in Haskell that are actually very very simple to understand and memorize:
fmap function operator version is <$>.fmap to ignore what’s inside a functor and replacing it with a constant, we use
fmap (const 32), for instance — or fmap (\_ -> 32) for the lambda version. You can use <$> too,
const 32 <$> [1, 2, 3]. But there’s a very logical and simple operator to remember. Notice how the
constant value is at the left of the <$> operator? Then, you can do the exact same thing with
32 <$ [1, 2, 3]. On the same level, if you prefer to put the value on the right side:
[1, 2, 3] $> 32. Simple.<|> operator. It has that | in it, which is often OR in most languages. And guess what: that
operator is the alternative operator. a <|> b equals a if it’s true or b if not. The definition
of truth is up to the type, but for Maybe a — Option<T> in Rust, true is Just _ (Some(_)) and
false is Nothing (None). See how easy it is?<?> operator. Once again, the ? reminds a question, information, etc.. operator in Haskell composes two functions. It looks closely to the ∘ math notation.>>=, which expects the function on the right hand side… Guess what the
=<< operator does. The exact same thing, but expects the function to be on the left hand side.</> operator allows to separate path parts without redundant /. The
<.> operator allows you to write the extension name!
"/home/phaazon" </> spareProjectsDir </> "rust/rfcs" </> lastRFC <.> "md".A huge topic, but basically, I hope that cargo can now resolve dependencies without accepting
several versions of a crate, but instead resolves them by traversing the whole dependency graph.
This is often needed on my side as I like to make a crate compatible with several dependency
versions, so that people who don’t update often can still have their clients benefits from updates
on my side. It’s expressed as SemVer ranges (e.g. stuff = ">=0.4, < 0.9") but cargo will take
the best one it knows. Basically, if you have such a dependency in project A and you depend on
B which has stuff = "0.5", then you will end up with both stuff-0.5 and stuff-0.8 in your
graph dependency, which to me is very wrong. Intersecting dependencies should only bring
stuff-0.5, because it’s the highest minimal version that satisfies every crates depending on it
in the dependency graph.
I talked about it here, but basically, I want to be
able to annotate trait’s items with visibility qualifiers (i.e. pub, pub(crate), etc.) so that
I can implement a trait in a crate without having people depending on my crate see the guts of the
trait.
Sealed traits prevent people from implementing the trait (first new concept) and private trait items both prevent people from implementing the trait but they also prevent them from seeing what’s inside.
Long discussion occurring here. Basically,
since features are parts of the language (both via cfg attributes and in Cargo.toml), it
would neat to be able to show them in rustdoc to help people discover what’s possible to do with
a crate, instead of opening the Cargo.toml on GitHub / whatever you’re using.
So that’s all for me and what I would love Rust to go towards. I have no idea whether people from the Rust project will actually read 10% of what I just wrote; I feel like I just made a wish list for Christmas.
Thank you for having read. Thank you for contributing to Rust and making it the best language in the world. And as always, keep the vibes and please let’s talk on Reddit! :)
]]>Disclaimer: this blog is about Rust and some of its intrinsics semantics, along with software design and architecture – especially for public interfaces like APIs. However, you may find it interesting to apply the concepts to any language that would support those ideas directly or indirectly.
I’ve been writing on a few examples code lately to add to documentations of some crates of mine. I write a lot of code that creates new objects that need other objects in order to be built. Most of the APIs you can see around tend to love the borrow principle – and I do. The idea is simple:
String, most APIs tend to
agree that the constructor of your type should take a &str or some sort of borrowing – typical
way to do that is to use the AsRef trait, that gives you more power (more about that below).
You can then just clone the &str in your function.Rc<_> already carries sharing and dynamic borrowing
semantics, so I wouldn’t be surprised to find a constructor that takes a Rc<_> instead of a
&Rc<_>.Depending on the crate you look at, the authors and how they see things, you might find a lot of ways to pass that string to your constructor. Let’s get technical. Especially, I want this blog post to give people a hint at how they should shape their APIs by driving the types with semantics.
struct Person {
ident: String
}
Very simple to get started. We have a Person type that carries the name of that person. We don’t
want to expose the internals of the type because we might add a lot of other things in there and
allowing people to pattern-match a Person would break their code when we add more things.
impl Person {
pub fn new(name: String) -> Person {
Person { name }
}
}
That works. This constructor works by move semantics: it expects you to move a String in in
order to copy it into the Person value. Move semantics are clear here: the client must allocate
a String or move one already allocated by someone else. This can get a bit boring. Imagine:
let someone = Person::new("Patrick Bateman"); // this won’t compile
Person::new("Patrick Bateman") doesn’t typecheck because "Patrick Bateman" has type &'static str here. It’s not a String.
So how can we fix the code above to have it compile?
let someone = Person::new("Patrick Bateman".to_owned()); // okay now
"Patrick Bateman".to_owned() makes a use of
ToOwned::to_owned to
clone the string. You could also have used
Into::into. However, we’re still
requiring clients of our API to perform the allocation – or move in, which is not ideal. A typical
solution to this is to use a borrow and clone when needing an owned version.
impl Person {
pub fn new(name: &str) -> Person {
Person {
name: name.to_owned()
}
}
}
This API enables the current code to compile:
let someone = Person::new("Patrick Bateman"); // okay because 'static is a subtype of 'a
We’re all good now, right? Not exactly: what happens if we try to pass the initial String we had
when we moved something in?
let x: String = …;
let someone = Person::new(x); // not okay now since it’s a type mismatch
To fix this, we need to pass a &str out of a String. Since
String implements Deref<Target = str>,
it’s quite straight-forward:
let x: String = …;
let someone = Person::new(&x); // okay, here &x derefs to &str
You can also use the more explicit
String::as_strmethod.
A nice trick here to be able to pass both &str and String is to use the AsRef trait.
impl Person {
pub fn new<N>(name: N) -> Person where N: AsRef<str> {
Person {
name: name.as_ref().to_owned()
}
}
}
In this case, AsRef<str> is implemented for both &str (it just returns itself) and String – it
just deref / uses the as_str method).
However, you can still see a problem here. If we keep using x after the call to Person::new,
this code is actually okay and we can move on. If we don’t need x afterwards, we’re just wasting
an opportunity to move in instead of allocating!
Clearly, to me, the perfect API would enable you to pass borrows and ask the API code to clone for
you or just accept the memory region you provide (i.e. you move something in). The idea is then to
accept either &str or String in our case, and clone only a &str… There are several traits and
types that provide that feature.
ToOwned + CowCow – for Clone on write – is a very interesting concept. It encodes that you’re either
borrowing some data or that you’ve already borrowed it. See it as:
enum Cow<'a, T> where T: 'a + ?Sized + ToOwned {
Borrowed(&'a T),
Owned(T::ToOwned)
}
impl<'a, T> Cow<'a, T> where T: 'a + ?Sized + ToOwned {
pub fn into_owned(self) {
match self {
Cow::Borrowed(b) => b.to_owned(),
Owned(o) => o
}
}
}
Now, the interesting part: it’s possible to go from &'a str to Cow<'a, str> and from String
to Cow<'a, str> by using Into implementors. That enables us to write this:
impl Person {
pub fn new<'a, N>(name: N) -> Person where N: Into<Cow<'a, str>> {
Person { name: name.into().into_owned() }
}
}
This code will move in a String if you passed one and clone if you passed &str. The following
lines of code compile and work as a charm:
let _ = Person::new("Patrick Bateman");
let dawg = "Dawg";
let _ = Person::new(format!("Doggo {}", dawg));
What’s interesting here is that all cases are covered:
Into<Cow<str>> on other types to pass other kind of arguments.There’s just a little nit: Cow::into_owned obviously patterns match on the variant, inducing a
small yet present runtime overhead. We tend to prefer using Cow<_> to dynamically dispatch the
decision to clone at runtime, while in our case, it’s more about a static choice (which API function
version to use).
Into, as simple as it getsIf you look at the Into<String> implementors, you’ll find impl Into<String> for String –
actually, this is a
blanket implementor –
and impl<'a> Into<String> for &'a str. That implements the same semantics as Cow<str>, but at
the type level, removing any remaining runtime overhead.
The new code is even simpler:
impl Person {
pub fn new<N>(name: N) -> Person where N: Into<String> {
Person { name: name.into() }
}
}
Obviously, there are drawbacks:
Into<String> trait bound. If you need, for any
reason, to dynamically check whether to clone or not, the Cow<str> is the right decision. If
you know you’ll always need to allocate, go for Into<String>.Into<String> is vague and some types might not even implement it.Into<String> doesn’t allow for cheap reading while Cow<str> does. Into<String> requires
you to move or allocate prior to reading.What I wanted to highlight in this blog post is that &_, AsRef<_>, Cow<_>, and Into<_> all
have different semantics that can be used to encode different contracts in public interfaces.
&T means that you don’t require your client to clone nor move because you might only perform
read-only computations on T. That gives some intuitions to your clients:
T.&[Something]. This carries one information more: contiguity of the input data – slices are
contiguous.Q: AsRef<T> is a &T on steroids in terms of software design. It gives more power to the
client as they’ll be able to pass more types in your functions. However, you now introduce a
polymorphic API. The semantics are the same: you plan to perform only read-only computations or
accept not to use a client-provided move-in value if you need to own something. Keep in mind an
important hidden property here: because you accept values to be moved in while you just have
a read-only contract on them, you also accept clients values to be dropped by corollary.Cow<T> states that the client can provide a borrow or an owned variable and you will take
advantage of that at runtime. This is important as it gives a good idea about how the memory
footprint of the function works: it will take decision at runtime whether or not it needs to own
memory.Q: Into<T> has vague and large semantics but can be used to encode the fact that you want
owned data, but you accept your clients to provide a borrow as long as you can clone. If they
provide you with an owned data, you’ll just do nothing (move in).Q: IntoIterator<Item = T> is also something you can use for the slices thing just
above. If (and only if) you don’t care about contiguity or will just inspect values one by one,
this is the interface to go. This interface gives hints that your function might iterate through
the input and perform (perhaps heavy) computation on each items before stepping to the next one.
Using IntoIterator instead of Iterator enables you the same kind of trick you have with
AsRef<_> vs. &_: you can take a Vec<_> directly instead of the iterator directly. Keep in
mind that this might not have sense for some types, especially if they have several iterator
interfaces. The str type has for instance the
str::bytes method that gives
you an iterator over bytes and the
str::chars method that yields
an iterator over UTF-8 characters.This list gives you a good idea about what interface you should use in your public interfaces.
Read-only? Owned data? Read and maybe write? All those semantics are visible through those types and
traits, you should definitely try to wrap your finger around using all of them. Of course, if you’re
just writing a small utility function that needs to borrow the .name of a Person, passing in a
&Person seems completely sound. Most of the time, if you don’t know where the data comes from,
be the more inclusive and generic as possible.
I hope you like that small article, and as always, keep the vibes.
]]>Imagine you want to expose a trait that defines an interface. People know about the interface and can use provided types to switch the implementation. That’s a typical use case of traits.
// This trait is pub so that users can see and use it
pub trait System {
type Err;
fn do_something(&mut self) -> Result<(), Self::Err>;
}
You can have a type SimpleSystem that implements System and that will do something special in
do_something, as well as a type ComplexSystem doing something completely different.
Now imagine that your System trait needs to expose a function that returns an object that must be
typed. What it means is that such an object’s type is tagged with another type. Imagine a
type Event that is typed with the type of event it represents:
pub struct Event<T> {
code: i64,
msg: String,
// …
}
That type must be provided and implemented by systems, not by the actual interface code. How would we do this?
Furthermore, we might want another trait to restrict what T can be, but it’s off topic for our
current problem here.
Let’s try a naive implementation first:
pub trait System {
type Event;
type Err;
fn do_something(&mut self) -> Result<(), Self::Err>;
fn emit_event(&mut self, event: Self::Event);
}
That implementation allows SimpleSystem to implement Event as a single type and then
implement emit_event, taking its Event type. However, that event is not typed as we
wanted to. We want Event<T>, not Event.
However, Rust doesn’t authorize that per-se. The following is currently illegal in Rust:
pub trait System {
type Event<T>;
// …
fn emit_event<T>(&mut self, event: Self::Event<T>);
}
RFC 1598 is ongoing and will allow that, but until then, we need to come up with a solution.
The problem is that associated types, in Rust, are completely monomorphized when the trait impl
is monomorphized, which is, for instance, not the case for trait’s functions. emit_event will
not have its F type variable substituted when the implementor is monomorphized — it will be sank
to a type when it’s called. So what do we really want to express with our Event<T> type?
pub trait System<EventType> {
type Event;
fn emit_event(&mut self, event: Self::Event);
}
That would work but that’s not exactly the same thing as our previous trait. The rest of the
trait doesn’t really depend on EventType, so we would duplicate code every time we want to support
a new type of event. Meh.
Event<T> is a higher-kinded type (HKT). Event, here, is what we call a type constructor — as
opposed to data constuctor, like associated new functions. Event takes a type and returns a
type, in the type-system world.
As I said earlier, Rust doesn’t have such a construct yet. However, we can emulate it with a nice hack I came across while trying to simulate kinds.
See, when a HKT has its type(s) variable(s) substituted, it becomes a regular, monomorphized type. A
Vec<T>, when considered as Vec<u32>, is just a simple type the same way u32 is one. The key is
to decompose our trait into two distinct yet entangled interfaces:
The first trait is implemented on systems and means “[a type] knows how to handle an event of a given type.” The second trait is implemented on systems, too, and means “can handle all events.”
pub trait SystemEvent<T>: System {
type Event;
fn emit_system_event(&mut self, event: Self::Event) -> Result<(), Self::Err>;
}
pub trait System {
type Err;
fn do_something(&mut self) -> Result<(), Self::Err>;
fn emit_event<T>(
&mut self,
event: <Self as SystemEvent<T>>::Event
) -> Result<(), Self::Err>
where Self: SystemEvent<T> {
<Self as SystemEvent<T>>::emit_system_event(self, event)
}
}
The idea is that the SystemEvent<T> trait must be implemented by a system to be able to emit
events of type T inside the system itself. Because the Event type is associated in
SystemEvent<T>, it is the monomorphized version of the polymorphic type in the actual
implementation of the trait, and is provided by the implementation.
Then, System::emit_event can now have a T type variable representing that Event<T> we
wanted. We use a special type-system candy of Rust here: Self: Trait, which allows us to state
that in order to use that emit_event<T> function, the implementor of System must also
satisfy SystemEvent<T>. Even better: because we already have the implementation of
emit_system_event, we can blanket-implement emit_event<T>!
Several important things to notice here:
System, to work as a system, obviously.SystemEvent<T>, to handle events of type T. The actual type is defined by the system itself.SystemEvent<T> if it doesn’t know how to handle
events of type T. That opt-in property is interesting for us as it allows systems to implement
only what they support without having to make our design bleed to all systems. Neat.Self: Trait allows to introduce a form of polymorphism that is not available without
it.emit_event, the implementor of System doesn’t even have to
implement emit_event.SystemEvent<T> has System has super-trait to share its Err type.The last point is the hack key. Without Self: Trait, it would be way harder or more convoluted
to achieve the same result.
Let’s see a simple implementor that will just print out the event it gets and does nothing in
do_something.
struct Simple;
#[derive(Debug)]
struct ForwardEvent<T>(pub T);
impl System for Simple {
type Err = ();
fn do_something(&mut self) -> Result<(), Self::Err> {
Ok(())
}
}
impl<T> SystemEvent<T> for Simple where T: std::fmt::Debug {
type Event = ForwardEvent<T>;
fn emit_system_event(&mut self, event: Self::Event) -> Result<(), Self::Err> {
println!("emit: {:?}", event.0);
Ok(())
}
}
As you can see, it’s really simple and straight-forward. We can select which events we want to be
able to handle: in our case, anything that implements Debug.
Let’s use it:
fn main() {
let mut system = Simple;
system.emit_event(ForwardEvent("Hello, world!"));
system.emit_event(ForwardEvent(123));
}
The idea is that, given a type S: System, we might want to emit some events without knowing the
implementation. To make things even more crunchy, let’s say we want the error type to be a
().
Again, it’s quite simple to do:
// we need this to forward the event from the outer world into the system so that we don’t have to
// know the actual type of event used by the implementation
impl<T> From<T> for ForwardEvent<T> {
fn from(t: T) -> Self {
ForwardEvent(t)
}
}
fn emit_specific_event<S, E>(
system: &mut S,
event: E
) -> Result<(), S::Err>
where
S: System<Err = ()> + SystemEvent<E>,
S::Event: From<E>,
{
system.emit_event(event.into())
}
fn main() {
let mut system = Simple;
system.emit_event(ForwardEvent("Hello, world!"));
emit_specific_event(&mut system, "Hello, world!");
}
We could event change the signature of System::emit_event to add that From<E> constraint to
make the first call easier, but I’ll leave you with that. The important aspect of this code snippet
is the fact that the implementor will handle a type of event Event<T> while the interface uses
T directly. We have injected a HKT Event.
Why do I care about such a design? It might seem complex and hard, but it’s actually a very useful use of a typeclass / trait type system — especially in luminance, where I use type-system concepts a lot; after all, it’s based on its Haskell version! If you compare my solution to the next-to-come HKT proposal from RFC 1598, we have:
type Event<T>; as associated types, which will allow to remove
our SystemEvent<T> trait.T here is the same
for all implementors: an implementor cannot restrict T, it must be implemented for all of them.
If you want to restrict the type of events your type can receive, if I understand the RFC
correctly, you’re handcuffed. That RFC will authorize the author of the trait to restrict your
T, not the author of the implementor. That might have some very useful advantages, too.T by applying constraints on
emit_event.In the quest of emulating HKT, I’ve found myself with a type-system toy I like using a lot:
equality constraints and self-constraints (I don’t know how those Self: Trait should be named).
In Haskell, since we don’t implement a typeclass for a type but we provide an instance for a
typeclass, things are slightly different. Haskell doesn’t have a Self type alias, since a
typeclasses can be implemented with several types variables (i.e. with the MultiParamTypeClasses
and FlexibleInstances GHC extensions), only equality constraints are needed.
In the end, Rust continues to prove that even though it’s a systems programming language, I can express lots of powerful abstractions I miss (a lot) from Haskell with a bit more noise. I think the tradeoff is worth it.
I still haven’t completely made up my mind about GAT / RFC 1598 (for sure I’m among the ones who want it on stable ASAP but I’m yet to figure out exactly how it’s going to change my codebases).
As always, have fun, don’t drink and drive, use condoms, keep the vibes and don’t use impl Trait in argument position. Have fun!
]]>![]()
OpenAL is an open-source and free audio library developped by Creative Technology. The first version, however, was made by Loki Software. The API targets spacialized audio and sound managing so that it’s simple to simulate sound attenuation over distance and Doppler effect. OpenAL is supported on a wide variety of platforms, from PC (Windows, Linux, FreeBSD, Mac OSX and so on) to consoles (PS3, Xbox…).
The latest version was released in 2005 (9 years old ago!) and is OpenAL 1.1. It’s then pretty logical that we should have OpenAL 1.1 in Haskell.
“Wait… we already have OpenAL in Haskell!”
That’s true. However, the OpenAL Haskell package has some serious issues.
The first one is that package is not a direct binding to OpenAL. It’s a
high-level wrapper that wraps stuff with obscure types, like StateVar or
ObjectName. Even though such abstractions are welcomed, we should have a raw
binding so that people who don’t want those abstractions can still use
OpenAL. Moreover, such high libraries tend to lift resources allocations /
deallocations into the GC1. That is a pretty bad idea because we,
sometimes, want to control how long objects live.
The second one, which is as terrible, is the fact that those abstractions aren’t defined by the Haskell OpenAL package, but by another, which is… OpenGL. The latter is not a raw binding to OpenGL – for that have a look at OpenGLRaw or gl – but the same kind of high-level oriented package. A lot of us have asked the authors of both the package to agree of making those abstractions shared between both the libraries; they never seemed to agree.
And even though, having a dependency between OpenAL and OpenGL is insane!
Sooooo… since Edward Kmett had the idea of gl to fix similar issues, I think I should have the idea of al to go along with gl. The idea is that if you want to use one, you can use the other one without any conflict.
Yesterday night, around 20:00, I decided to make it. It took me several hours to write the raw binding, but I finally made it and released al 0.1 the very next day – uh, today!
Because OpenAL is not shipped with your 2 nor anything special, and thus
because you have to explicitely install it, installing
al requires a bit more than just
typing cabal install al.
First things first:
cabal update
You should have al available if
you type cabal info al afterwards.
al requires you to have c2hs – version 0.23.* – installed. If not:
cabal install c2hs
Be sure it’s correctly installed:
c2hs --version
C->Haskell Compiler, version 0.23.1 Snowbounder, 31 Oct 2014
build platform is "i386-mingw32" <1, True, True, 1>
Of course, you should have the OpenAL SDK installed as well. If not, install it. It should be in your package manager if you’re on Linux / FreeBSD, and you can find it here for Windows systems.
Important: write down the installation path, because we’ll need it!
Here we are. In order to install, al needs the paths to your OpenAL SDK. I’m not sure whether it could vary a lot, though. The default Windows paths are:
C:\Program Files (x86)\OpenAL 1.1 SDK\libs for the libraries ;C:\Program Files (x86)\OpenAL 1.1 SDK\include for the header files.In future releases, I’ll default to those so that you don’t have to explicitely pass the paths if you have a default installation. But for the moment, you have to pass those paths when installing:
cabal install al --extra-lib-dirs="C:\Program Files (x86)\OpenAL 1.1 SDK\libs"
--extra-include-dirs="C:\Program Files (x86)\OpenAL 1.1 SDK\include"
Of course, you may adapt paths to your situation. You may even try without that
for UNIX systems – it might already be in your PATH, or use slashs for
MinGW.
al lays into two module trees:
Sound.ALSound.ALCThe former exports anything you might need to use the OpenAL API while the latter exports symbols about the OpenAL Context API. Please feel free to dig in each tree to import specific types or functions.
al doesn’t have any documentation for a very simple reason: since it’s a raw binding to the C OpenAL API, you should read the C documentation. Translating to Haskell is straightforward.
Up to now, al doesn’t cover extensions. It’s not shipped with ALUT either as I want to keep the package low-level and raw – and don’t even try to make me change my mind about that ;) .
If you have any issues, please let me know on the issues tracker. I’m responsive and I’ll happily try to help you :) .
Once again, have fun, don’t eat too much chocolate nor sweets, and happy Easter Day!
Garbage Collector
Operating System
Hello people. It’s been weeks I have started to work on luminance-1.0.0. For a brief recap, luminance is a graphics crate that I originally created in Haskell, when I ripped it off from a demoscene engine called quaazar in order to make and maintain tiner packages. The Rust port was my first Rust project and it became quickly the default language I would develop graphics applications in.
So if you’re interested, yes, luminance in Haskell still exists but I maintain it for strictly minimal support (i.e. support compiler bumps, for instance). The default and official language to use, if you’re interested by luminance, is Rust.
Another important thing I have to explain is the current feature list of luminance and what it’s going to be soon. Currently, luminance’s goals are (according to the documentation):
That’s the current situaton. A very important other aspect is that luminance was designed, in the first place, to be backend agnostic. I quickly decided not to go this way and stick to a simpler design with OpenGL only.
A comparison between luminance and other famous crates is available here.
However, I have — again! — changed my mind. Today, I want to have a system of backend in luminance. Maybe not something as complex and complete as the gfx-rs project, but something that will allow those backends:
I really do not target Metal, DirectX12 nor any other graphics backend you might have in mind. However, if you think it’s possible to do so, then we should discuss about it. Nevertheless, as I said above, luminance is need-driven: as I don’t need DirectX12, I will not work on it unless someone REALLY needs it and provides a PR (or at least enough for me to work on it… including beers).
luminance-1.0.0 will ship… as soon as possible. The current feature set is huge, though:
"std" feature-gate. This will help use luminance with specific contexts, especially
for demoscene or size-limited devices. Currently, the work on no_std has been saved for later
as I had hit a bug last time I checked.Tess interface. Now, a TessBuilder must be used. This allows for three major
enhancements:
unsafe traits easily, without writing any unsafe code. Among those:
Vertex trait, used to create vertex types.Semantics trait, used to create vertex semantics types.UniformInterface trait, used to create shader uniform interfaces.macro_rules are removed.GL33 driver
for OpenGL 3.3, a GL40, GL44, GL45 etc., but you can also have WebGL and VK10.Tess — instead of using the current method with GPU Buffer<_>. This was
asked by several people and I just couldn’t ignore such an interesting and useful feature!TriangleFan or LineStrip and “cut” a primitive if an index is equal to a given value. This
is very useful for implementing terrains, quadrics or a lot of other nice things.Vertex type and the order
in which the fields appear in the struct defines the bindings the GPU will present to
shaders… but it’s the user responsibility to tell how the shader will fetch those attributes.
This can lead to very wrong situations in which, for instance, the GPU present the normal
attribute as having the index 4 but the shader tries to fetch them on index 2. Vertex
semantics fix this situation by adding an intermediate layer both the user and the GPU (i.e. via
luminance) must speak: semantics. Semantics are user-defined and none is hardcoded in
luminance. As soon as a type uses vertex semantics, all the type system knows how to forward
that information to shaders and what shaders should do to fetch them. The other bonus, ultra
cool thing is that shaders and memory buffers now compose way better: since vertex semantics
define a sort of namespace, you will never have two shaders using the same index for different
semantics (unless you use completely different vertex semantics type). A very cool feature I’m
proud of and that I will detail in a future article / luminance tutorial.You might be wondering “Woah, this is such a big feature set. A lot of things are coming! Why not having split the feature set into several releases?”, and that’s a good question.
Lately, I’ve been feeling on-and-off about everything I do on my spare-time on the FOSS level. I have several crates I care about and sometimes people show up and start asking for features and/or bug fixes. This takes some time and after a long day of work, I sometimes feel that I just want to hack on things I like, play my electric guitar, go to swim or wakeboard or just spend time with people I care about and love.
Currently, luminance — even though it has an unexpected amount of downloads! — is not a famous crate. People nowadays talk about crates living in a higher-level stack, such as ggez, gfx, amethyst, piston, etc. I don’t communicate a lot about luminance because of, mostly, two things:
I guess Haskellers will get the second point. ;)
My own philosophy about software and especially my own software is that it should have a top-tier
quality, both in terms of API design, elegance, type safety, performance and documentation. The
front documentation of warmy is a good example of something I continuously
seek, because good documentation helps my future myself to read the code and public interface but
also for onboarding people. Lots of projects — especially in professional industries — suffer from
knowledge bubbles, in which just a bunch (often only one!) of people know about A and B and
when someone joins in, they’re lost and have to ask questions. As an engineer, I always write
everything I know in whatever source-of-truth the company I’m in can offer (it can be a Confluence
server; a git repository; a wiki; whatever) because sharing knowledge and writing processes is key
in engineering — also, pro tip: I highly value that when interviewing candidates!
But the more I advance and grow up, the more things I learn and the more things I master. And as I master new things, I tend to get used to them. And the “mental effort” it requires tends to slowly disappear. I remember when I tried (hard!) to wrap my finger around what a profunctor is, or what free monads or even natural transformations are and how to use them. It was hard. As a friend of mine (@lucasdicciocio) perfectly summarized it some times ago, when we learn something as a beginner, we struggle. But people who master those concepts struggle even harder when trying to learn the next concepts at hand. And I’m not an exception. Today, free monads are something I know (even though I don’t use them; it’s a myth! it’s a trap! :D) and I feel like I’m not legitimate to talk about them because they feel easy — and it’s normal, since I’ve learned them. But I see new things to learn, new hard things that I know I’ll be struggling with for a while. And now it feeds the imposter syndrom. You know, when you’re 20, you feel like you know a lot of things (and you likely do) and that you’re pretty confident with your skill base. Today, I’m 27, and I feel like I will always be a rookie, because there is always something I don’t know to struggle with. And I find that very exciting. Of course, I don’t project that on others, which is soooo paradoxal: I don’t expect a newcomer to tell me what a fundep is or know lifetime elision rules in Rust. But I guess I have a very very strict way to judge my knowledge. I still haven’t figured out if it’s a good thing or if it’s not.
I’ve been told that talking about what I do, what I think, what I know and my engineering philosophy should help with this kind of questioning. So I decided to use that blog article to share some thoughts about the next release of luminance that took me so much of my spare-time (but happily!) and to talk a bit about myself. I know a lot of people in the Rust and Haskell community don’t know me, but I also know lots of other folks do know me, in both communities. I hope my little vent will give people some relief — maybe you too have a similar experience? Please share your comments on the Reddit thread this article is post in.
As always, stick doing awesome things and spend your spare-time wisely. Spend time with the ones you love and do the things you hecking love too!
Keep the vibes!
]]>std::unique_ptr abstraction. I needed such a type because I was handling scarce resources that
needed to be uniquely owned in our code base, but at the same time, checking every time whether
a value is present (nullptr being the problem here) yields large numbers of bugs in production.
They asked “What’s a safest std::unique_ptr?” My main problem was (still is) that
std::unique_ptr can be initialized with nullptr. The problem with this is that you cannot
assume that you always own a T if you are given a std::unique_ptr<T>. References and wrapped
references cannot be used either because we really want to own the data here.
The rest of the discussion is driven by that initial motivation.
As a (mainly) Haskell and Rust developer, I’m also pretty good at writing C and C++ — after all, I’ve been doing C and C++ since I’m 11 and today, I’m 28; I’ll let you do the math. I especially developed 3D stuff, ranging from toy experiments to demoscene productions, and lots of other low-levels things I won’t detail here.
See, I’ve failed a lot while trying things when I was younger — especially around the age of 15 / 16. That’s basically the age where I started to be very frustrated at C++ and, without even knowing it, was asking to find a better way to design code. Yes, I failed a lot, I have no shame stating it. But to me, failing has been a building block to my experience. The more I fail, the less likely I’ll fail again the same way. By definition, if you fail a lot on different things… you now have very good hints and ideas about how not to fail again. This is like learn the hard way, but I truly think it’s important, especially to get why something doesn’t work. Reading books and being told is important, but failing is even more.
Yes, today, with all the hindsight I’ve been through, all the languages I’ve been using – ranging from C/C++, D, Python, Haskell, Idris, OCaml, Rust, Java, JavaScript, Perl, C#, GLSL and several others — I think I have a pretty good idea about what’s out there in terms of language ideas.
The goal of this article is to explain exactly why I don’t like that much C++, why I think it has a lot of bad ideas, and to provide people with material to think and meditate. I truly think that a good developer needs to call themselves into question regularly and question their process and established knowledge. I think that, decades ago, C and C++ were the right way to do things. Today, I think those languages and especially what has been accepted as ground and established truth about how to do things has become obsolete.
I’m going to explain why in a series of blog articles, starting with this one. Let’s go.
I want to start with a very good example of something that has been accepted as pretty nice when it was introduced and that is completely obsolete to me. Let’s talk about C++’s constructors. But not constructors by themselves. I want to focus on how most people use them, especially around the concept of fallible construction — we’re going to talk about exceptions, too!
For those who are not familiar with constructors in C++, let me explain a bit. In C, when you have
a data type (like a struct), you can create an object with such a type by allocating memory for it
(either on the stack or the heap) and then initialize its fields. Since C99, you also have
the designated initializer syntax to initialize a struct directly while you’re declaring it.
Some examples:
struct Foo {
int x;
char const* y;
};
int main() {
Foo foo;
foo.x = 3;
foo.y = "Hello, world!";
// designated initializer syntax
Foo foo2 = {
.x = 3,
.y = "Hello, world!"
};
}
Even though that code seems pretty harmless, to me, it’s very easy to misuse C’s construction syntax. I will not explain exactly why here because we’re interested in C++, but you’ll understand what’s the problem by reading on. It relates to being possible to leave a state or part of a state undefined without having your compiler prevent you from doing that.
In C++, you cannot go with any of those way to create a value without a mandatory C++ concept: constructors. Constructors are C++’s way to initialize objects by using the same kind of syntax. C++ has several constructors:
foo which type is Foo to a
function that expects a Foo, for instance.Foo &&).
You can obtain such references by using std::move or std::forward.There is a slightly exception: aggregate initialization, which is a bit similar to the C’s designated initialization, but requires that you have not declared any constructor – among other restrictions — and is then always available, either for public or private code. I will then not talk about it. I also have to admit I never completely recall all the rules about POD designated initialization.
Let’s demonstrate some of the constructors from above in the following program:
#include <iostream>
struct Foo {
// default constuctor
Foo() {
std::cout << "default ctor called" << std::endl;
}
// copy constructor
Foo(Foo const &) {
std::cout << "copy ctor called" << std::endl;
}
// move constructor
Foo(Foo &&) {
std::cout << "move ctor called" << std::endl;
}
// assignment operator by copy
Foo & operator=(Foo const &) {
std::cout << "operator= called by copy" << std::endl;
return *this;
}
// assignment operator by move
Foo & operator=(Foo &&) {
std::cout << "operator= called by move" << std::endl;
return *this;
}
};
Foo get_foo() {
return Foo();
}
int main() {
Foo foo;
auto foo2 = foo;
auto foo3 = get_foo();
return 0;
}
In the main, what constructors do you think each line is going to call? :) The answer is… it
depends on your compiler. Yes, you heard me right. With my compiler, this is what I get:
Foo foo;: default constructor, which should be the case for every compiler here.auto foo2 = foo;: copy constructor.auto foo3 = get_foo();: default constructor. This is due to something called (N)RVO.This is already pretty bad, for several reasons:
std::vector<Something>, which might contain thousands of millions of Something.
Because C++ copies by default with the value semantics, you might end up with several copies
while your code doesn’t explicitly copy anything.auto foo = something; does —
and I feel them, I often spend some time trying to figure out as well.The existence of copy constructors and the value semantics by default makes C++ hard to reason about when we are making copies. Other languages, such as Rust, mitigate that risk by using a move semantics by default. If you want to copy (i.e. clone) something, you have to explicitly ask for it. For instance, consider:
// C++
void foo(std::vector<Foo> foos) {
// …
}
int main() {
std::vector<Foo> foos;
// …
foo(foos);
return 0;
vs.
fn foo(foos: Vec<Foo>) {
// …
}
fn main {
let foos = Vec::new();
// …
foo(foos);
}
The C++ program will copy the content of foos when calling foo. The copy here is due to the
copy constructor of std::vector being called. The effect of is that the whole content of the
heap-allocated region owns by the vector will be copied. I typically call that a deep copy, or
simply a clone.
The Rust program will not make a copy of foos. Instead, it will move ownership from the caller
to the calle. foos won’t be available anymore in main after the call to foo. So no heap
allocation will occur here. If we wanted the same behavior as the C++ one, we would do this:
foo(foos.clone());
That requires that Foo implement the Clone trait, but it’s off topic here. However, there a
few places where Rust can still do hidden copies that can hurt your performance. By default,
everything is moved. However, if your type implements the Copy trait, it means that it can be
safely copied. Such a copy is always performed bit-wise. What’s interesting is that there is, in
theory, nothing different at runtime between copying and moving in Rust. Moving might allow
more optimizations to prevent actually copying data, but imagine you don’t have optimizations.
Both copy and move semantics, in Rust, could be implemented with a memcpy. The main difference
is that copying doesn’t lose ownership. So, because the original value, is not moved, can still be
used after copy. Rust has implementations of Copy for arrays, which, thus, can yield bad
performances if passed directly to a function, for instance:
fn bar(_: [u32; 1000000]) {}
Calling bar will make a copy of the array you pass as argument, which is bad news here. However,
I think it’s pretty unlikely that you allocate such an array on your stack. Most non-primitive
types from the standard library don’t have an implementation for Copy, which makes move follow
the move semantics.
Let’s get back to C++, shall we. We’ve established that C++ makes it very easy to write code which has hidden copies, because of the existence of copy constructors and that value semantics depends on it. Let’s go on with a problem I have with constructors that is as bad to me.
Imagine that you want to be able to manipulate a kind of integer. That integer cannot be negative
and it cannot be equal to 0. Also, we want it on 32-bit precision. There are several ways to do
that. Let’s enumerate a few of them (non-exhaustive list):
int32_t type from the standard library and every time we try to create or modify
it, forbid to put values which are less or equal to 0.uint32_t type from the standard library and prevent using 0. We don’t need to check
for negative values because uint32_t cannot encode them.1.Obviously, both (1.) and (2.) have a big drawback: you’re going to spend a lot of runtime to check
that the invariant is not violated. Worse, the invariant leaks in the API. They have an API
contract flaw: they don’t say to the programmers they cannot be equal to 0. For instance:
void foo(uint32_t non_zero) {
// …
}
Even though the name of the variable is non_zero, what prevents a user from calling foo with
0? Nothing. They can just call foo(0) and the code will still compile. They will then get a
really bad runtime undefined behavior, or maybe nothing for several runs and a sudden bug… or you
will have to handle the invariant at every call, which doesn’t seem like a lot of fun — and it’s
dangerous because you can forget to check it.
Instead, we want this:
void foo(NonZero non_zero) {
}
Here, the function will clearly not be callable with a uint32_t. So the provided value must be
statically verified to be non-zero. The NonZero wrapper type must then ensure the invariant is
non-violated. I now have a question: do you think NonZero has to check the invariant every time
it’s used, or we can do better?
C++ states that NonZero should be constructed with a constructor, and my point is that
constructors are broken, because not all values can be constructed via C++ constructors. But they
are not broken the way you think. In theory, and actually, there is a way to use C++ constructors
in sound ways. Try to spend a few minutes and think about it. Can you implement a struct NonZero
with an API exposing only constructors to create and initialize non-zero values? There are two
possibilities:
NonZero, and because it’s very easy to build a NonZero with a violated invariant with your
code compiled correctly: auto foo = NonZero(0);. So, it’s a no.I want to be very very crystal clear here. Exceptions in C++ are heavy. They require unwinding most
of the time when some code throws them. They are needed to handle exceptional errors, such as an
out of memory error, or a dead thread. People have been abusing exceptions for too long. Really.
Stop using exceptions. Calling NonZero(x) with x being 0 is an error that originate from
several places, because NonZero is a pretty abstract concept. You shouldn’t assume it’s
exceptional that someone tries to build one with 0. It’s part of its API: it must fail to
construct if built with 0. The API must convey the fact it can handle that kind of error. So
it’s an error that is covered by the scope of the type and its carried invariant.
An example of exceptional error here would be that, when you construct your NonZero, you’re out
of space on the stack. Throwing an error here seems okay to me. The problem, here, is not that we
have a constuctor that fails. The problem is that we used a constructor to encode fallible
construction. That’s the problem. My point here is that constructors must not be used for
fallible constructions.
So… what are we left with? If you really want to only use constructors without exceptions,
you have no other choices but to accept to have a dangling invariant. The invariant here is that
the wrapped uint32_t must not be 0.
A small note on invariants: they must be held before and after you call a public function on your type. We can violate them inside our private code for performance and optimization purposes, but don’t forget that you must rebalance everything so that the invariant is held when you give control back to the user. This is especially hard to do in C++ because of exception safety, so a rule of thumb: try to never violate invariants, even in private code.
struct NonZero {
// prevent people from creating default non-zero values; it makes no sense
NonZero() = delete;
NonZero(uint32_t value): _wrapped(value) {
// that would break your application at runtime on debug and do nothing on release… :(
assert(_wrapped != 0);
}
// let C++ automatically implement some stuff for us, which won’t break the invariant
NonZero(NonZero const &) = default;
NonZero(NonZero &&) = default;
NonZero & operator=(NonZero const &) = default;
NonZero & operator=(NonZero &&) = default;
private:
// the invariant must be held on this value
uint32_t _wrapped;
};
As you can see, having a NonZero with this definition doesn’t mean the value cannot be 0.
Example:
int main() {
auto nz = NonZero(0); // meh
return 0;
}
This code compiles fine and will either abort at runtime on an assert or just silently do something very wrong on release code.
So what can we do to this situation?
The thing is that, if you’ve only done C++ in your life, you’re likely to think that the best thing to do is to give up and use exceptions. After all, they’re pretty nice.
#include <stdexcept> // you’ll need that for the exception type
struct NonZero {
// prevent people from creating default non-zero values; it makes no sense
NonZero() = delete;
NonZero(uint32_t value): _wrapped(value) {
if (_wrapped == 0) {
throw std::invalid_argument("cannot create a NonZery with 0");
}
}
// let C++ automatically implement some stuff for us, which won’t break the invariant
NonZero(NonZero const &) = default;
NonZero(NonZero &&) = default;
NonZero & operator=(NonZero const &) = default;
NonZero & operator=(NonZero &&) = default;
private:
// the invariant must be held on this value
uint32_t _wrapped;
};
Now, when you call NonZero(0), an exception is thrown. You can catch it with a try / catch
block. But please wait a minute. Several points:
throw — and again, it could quickly get hard because you
might call functions that throw too…That last point is what makes C++ a very hard language what it comes to error handling to me.
Error handling, most of the time, is based on exceptions, which are invisible at the type-level.
I got some remarks stating that, nowadays, modern C++ code bases use the noexcept keyword when
something cannot fail, and the rest of the time, we must assume something can fail. Okay, but
then, fail how? What kind of error? How do I know that? Rely on whether it’s written in a
Doxygen documentation, that, most of the time, doesn’t even exist? Also, just have a look at how
people handle failures in C++. Do you see those try / catch everywhere noexcept is not
annotated? No, because people don’t use noexcept and when they use library, they don’t even
apply that advice to themselves either. The problem with exceptions not being visible in types and
function signatures is that you have to read the code (or the missing Doxygen documentation :)) to
find out what exceptions you need to catch. The other problem is that they can be ignored and
implicitely and automatically propagated upwards in the call stack. There is no way to force a
user to either handle the error or explicitly pass it to their caller.
All that to say: don’t do this. Don’t think constructors are a good idea to construct types in all possible situations. Failible constructions shouldn’t be done with public constructors and exceptions. However, it doesn’t mean constructors cannot be used to implement fallible constructions. That seems tricky, and you’re right. You might wonder:
“Okay okay, you’ve advanced points but… still, how would you do it?”
My implementation — and today, I would only accept that one — to this problem is this: since we
cannot create a value without using a constructor, and since constructors fail to encode fallible
constructions without using exceptions — which I don’t accept either, I’m going to use a private
constructor to build my NonZero and not check the invariant. Then, using a static method, I
can return a more typed object to encode a possible failure, by checking the invariant before
creating the object.
Consider:
#include <optional>
struct NonZero {
// prevent people from creating default non-zero values; it makes no sense
NonZero() = delete;
// let C++ automatically implement some stuff for us, which won’t break the invariant
NonZero(NonZero const &) = default;
NonZero(NonZero &&) = default;
NonZero & operator=(NonZero const &) = default;
NonZero & operator=(NonZero &&) = default;
// static method used to create a NonZero; the only way to create one
// using the public interface
std::optional<NonZero> from_u32(uint32_t value) {
std::optional<NonZero> r;
if (value != 0) {
// call to the private ctor; the invariant is already checked in this branch
r = NonZero(value);
}
return r;
}
private:
// private constructor that doesn’t check the invariant
NonZero(uint32_t value): _wrapped(value) {}
// the invariant must be held on this value
uint32_t _wrapped;
};
So let me explain a bit all this code. First, the NonZero(uint32_t) constructor is private. It
has to be private because it doesn’t enforce the invariant. It allows to construct any NonZero.
We need it because C++ requires you to use a constructor to create a value of type NonZero.
Because I don’t want exceptions, I just don’t check the invariant here so that constructor
cannot fail.
The static method called from_u32 is the only entry-point to create a value of type
NonZero. As you can see, it returns a std::optional<NonZero>, which means that it can fail.
If you call that function with 0, you will get an empty optional value. That function is
implemented by creating an optional value, allocated on the stack with the default constructor —
which makes it empty. Then, we check the invariant and if it’s not violated, we allocate and
initialize the NonZero, and return the whole thing.
For the rest of the article, consider we add the following method to NonZero:
uint32_t value() const {
return _wrapped;
}
If you’ve followed carefully what we’ve been doing here, a call to NonZero::value() will never
return 0, because construction statically disallows building such non-zero values. You then don’t
have to check for it!
However, we’re not completely done. We’ve used the std::optional standard type. How are we
supposed to use that type? Looking at the official documentation, we get use the operator* or
operator-> to access the
underlying object. We have the
has_value() method to check
whether a value is present.
auto nz = NonZero::from_u32(0);
if (nz.has_value()) {
std::cout << "We have a value! " << nz->value() << std::endl;
}
That seems nice, right? Well, not really. C++ doesn’t have exhaustive pattern matching, which
makes it impossible to statically ensure you exhaustively check a std::optional. What it means
is that, given the current contract based on has_value() and the dereference operators, we
can still break our program and bring undefined behaviors without having our compiler complain.
auto nz = NonZero::from_u32(0);
std::cout << "We have a value… right? " << nz->value() << std::endl;
That program is perfectly fine and valid. However it’s not. It’s completely wrong, because
std::optional’s API allows developers to access a value that might not be there.
This is the same situation as doing this:
int * ptr = nullptr;
int x = *ptr;
Statically fine. Dynamically pretty bad, right? C++’s standard library’s std::optional could
have been done in a much safer way, by, for instance, providing you with some combinators to
work with the underlying type. C++ doesn’t have exhaustive pattern matching but it still has
higher-order functions. We could then have something like:
auto nz = NonZero::from_u32(0);
nz.maybe(
// called if no value is present
[]() {
std::cout << "No value there." << std::endl;
},
// called if a value is present
[](NonZero const &v) {
std::cout << "We have a value: " << v.value() << std::endl;
}
);
For most C++ developers, that code will look like very verbose and wrong, and I agree. Without
pattern matching, it’s very hard to safely and statically use a std::optional.
In Rust, we have exhaustive static dispatch. std::optional is called Option and Rust
doesn’t have constructors at all.
Values can be constructed by providing all fields, if they are all available (i.e. that’s
always the case in the module the type is defined in; for the rest, you need access to the fields
by marking them all pub).
This is my NonZero wrapper in Rust:
struct NonZero {
// we don’t make that value pub so no one can directly construct a NonZero
// outside of our module
wrapped: u32,
}
impl NonZero {
// yes, new is nothing special in Rust, so we can use it as a method!
// Self references the current type we’re adding an impl for (here, NonZero)
pub new(wrapped: u32) -> Option<Self> {
if wrapped == 0 {
None
} else {
Some(NonZery { wrapped })
}
}
}
fn main() {
match NonZero::new(0) {
Some(value) => println!("we have a value! {}", value),
None => println!("we don’t have a value… :("),
}
}
That’s all. The program is statically safe, as the compiler can make sure we’re correctly using
the Option<NonZero>. We could use combinators that do the pattern matching for us. For
instance, imagine that you want the value 8080 if it’s not correctly constructed, or what it
contains otherwise:
let port = nz.unwrap_or(8080);
Here, the type of port is NonZero. Option::unwrap_or might be implemented like this:
impl<T> Option<T> {
pub fn unwrap_or(self, default_value: T) -> T {
match self {
Some(v) => v,
None => default_value,
}
}
}
There are a lot of similar combinators, like Option::map, Option::or, Option::unwrap_or_else,
etc. I’ll let you dig by yourselves. However, there is an important point to make here. In C++,
there is the std::optional::value() method that returns the value if it’s present or just throws
an error (which I dislike). In Rust, there is the std::Option::unwrap() method, that returns the
value if it’s present or panics otherwise… which I also dislike. unwrap() can be dangerous in
Rust and I think it should be marked unsafe. But a more detailed article is needed about why
I want it to be marked unsafe and it’s off topic anyways.
So, we’re hitting the end of the article. It might be a lot of information; sorry. To sum up, in C++:
std::optional or similar,
and enforce invariants / pre-conditions.Next time, I’ll bring you on a tour with me around the concept of inclusion polymorphism, which is most of the time called inheritance in the world of OOP languages. But we’ll still stick to C++. ;)
]]>
I’ll also give a snippet you can use to load geometries with wavefront and adapt them to embed into
luminance so that you can actually render them! A package might come up from that kind of snippet –
luminance-wavefront? We’ll see that!
This package has received several changes among two major increments and several fixes. In the first
place, I removed some code from the interface that was useless and used only for test purposes. I
removed the Ctxt object – it’s a type used by the internal lexer anyways, so you don’t have to
know about it – and exposed a type called WavefrontOBJ.
That type reprents the parsed Wavefront data and is the main type used by the library in the
interface.
Then, I also removed most of the modules, because they’re re-exported by the main module – Codec.Wavefront. I think the documentation is pretty straight-forward, but you think something is missing, please shoot a PM or an email! ;)
On the bugs level, I fixed a few things. Among them, there was a nasty bug in the implementation of an internal recursive parser that caused the last wavefront statement to be silently ignored.
I’d also like to point out that I performed some benchmarks – I will provide the data later on with a heap profile and graphs – and I’m pretty astonished with the results! The parser/lexer is insanely fast! It only takes a few milliseconds (between 7ms and 8ms) to load 50k faces (a 2MB .obj file). The code is not yet optimized, so I guess the package could go even faster!
You can find the changelog here.
I made a lot of work on luminance lately. First, the V type – used to represent vertex
components – is not anymore defined by luminance but by linear.
You can find the type here.
You’ll need the DataKinds extension to write types like V 3 Float.
That change is due to the fact linear is a mature library with a lot of interesting functions and
types everyone might use when doing graphics. Its V
type has several interesting instances – Storable, Ord, etc. – that are required in luminance.
Because it’s not simple to build such V, luminance provides you with three functions to build the
1D, 2D and 3D versions – vec2,
vec3
and vec4.
Currently, that type is the only one you can use to build vertex components. I might add V2,
V3 and V4 as well later.
An interesting change: the Uniform typeclass has a lot of new instances! Basically, all vector
types from linear, their array version and the 4x4 floating matrix – M44 Float. You can find the
list of all instances here.
A new function was added to the Graphics.Lumimance.Geometry
module called nubDirect.
That function performs in linear logarithmic time and is used to turn a direct representation of
vertices into a pair of data used to represent indexed vertices. The new list of vertices
stores only unique vertices and the list of integral values stores the indices. You can then use
both the information to build indexed geometries – see
createGeometry
for further details.
The interface to transfer texels to textures has changed. It doesn’t depend on Foldable anymore
but on Data.Vector.Storable.Vector. That change is due to the fact that the Foldable solution
uses toList behind the hood, which causes bad performance for the simple reason that we send the
list to the GPU through the FFI. It’s then more efficient to use a Storable version. Furthermore,
th most known package for textures loading – JuicyPixels
– already uses that type of Vector. So you just have to enjoy the new performance boost! ;)
About bugs… I fixed a few ones. First, the implementation of the Storable instance for
(:.)
had an error for sizeOf. The implementation must be lazy in its argument, and the old one was not,
causing undefined crashes when using that type. The strictness was removed and now everything
works just fine!
Two bugs that were also fixed: the indexed render and the render of geometries with several vertex components. Those bugs were easy to fix and now you won’t experience those issues anymore.
I thought it would be a hard task but I’m pretty proud of how easy it was to interface both the
packages! The idea was to provide a function that would turn a WavefrontOBJ into a direct
representation of luminance vertices. Here’s the function that implements such a conversion:
type Vtx = V 3 Float :. V 3 Float -- location :. normal
objToDirect :: WavefrontOBJ -> Maybe [Vtx]
objToDirect obj = traverse faceToVtx (toList faces)
where
locations = objLocations obj
normals = objNormals obj
faces = objFaces obj
faceToVtx face = do
let face' = elValue face
vni <- faceNorIndex face'
v <- locations !? (faceLocIndex face' - 1)
vn <- normals !? (vni - 1)
let loc = vec3 (locX v) (locY v) (locZ v)
nor = vec3 (norX vn) (norY vn) (norZ vn)
pure (loc :. nor)
As you can see, that function is pure and will eventually turn a WavefrontOBJ into a list of
Vtx. Vtx is our own vertex type, encoding the location and the normal of the vertex. You can
add texture coordinates if you want to. The function fails if a face’s index has no normal
associated with or if an index is out-of-bound.
And… and that’s all! You can already have your Geometry with that – direct one:
x <- fmap (fmap objToDirect) (fromFile "./ubercool-mesh.obj")
case x of
Right (Just vertices) -> createGeometry vertices Nothing Triangle
_ -> throwError {- whatever you need as error there -}
You want an indexed version? Well, you already have everything to do that:
x <- fmap (fmap (nubDirect . objToDirect) (fromFile "./ubercool-mesh.obj")
case x of
Right (Just (vertices,indices)) -> createGeometry vertices (Just indices) Triangle
_ -> throwError {- whatever you need as error there -}
Even though the nubDirect performs in a pretty good complexity, it takes time. Don’t be surprised
to see the “loading” time longer then.
I might package those snippets and helpers around them into a luminance-wavefront package, but
that’s not trivial as the vertex format should be free.
I received a lot of warm feedback from people about what I do in the Haskell community, and I’m just amazed. I’d like to thank each and everyone of you for your support – I even got support from non-Haskellers!
What’s next then… Well, I need to add a few more textures to luminance – texture arrays are not
supported yet, and the framebuffers have to be altered to support all kind of textures. I will also
try to write a cheddar interpreter directly into luminance to dump the String type of shader
stages and replace it with cheddar’s whatever will be. For the long terms, I’ll add UBO and SSBO to
luminance, and… compatibility with older OpenGL versions.
Once again, thank you, and keep the vibe!
]]>I came into Zellij a wild ago, while I was looking for something more modern over tmux. It had the terminal multiplexer features (windows, splits, focus, etc.), but was lacking features (sessions, for instance, were added mid-2021²). As time passed, it started to get new features and promising. For instance, its layout system, allowing to have a more minimalistic view (the default one being really all-over-the-place).
And then I realized that some very basic features from tmux were absent:
prefix+z by default in tmux; ToggleFocusFullscreen action in Zellij), in Zellij, I don’t
have any indication the pane is being focused, while tmux has this nice and neat Z marker in the statusbar. That
doesn’t seem like important, but it is, especially when you keep zooming in and out.What I like about a software, like, for instance, tmux or kakoune, is that they are minimal. They do one thing and they do it well. If you are not sure about whether something is minimal, ask yourself a simple question: “What do we expect from this tool to solve; to do?”. For a terminal multiplexer, we expect:
If you provide a few amounts of nice features, and composability tools, then your users can build more by composing
your system with others. For instance, tmux has the display-popup (popup) command that takes a command to run
and displays it in a floating window. That’s great, because now I can do something like tmux ls -F '#S' to list all
running sessions, pipe it to my favorite fuzzy picker (fzf, sk, etc.), and redirect the result to
xargs tmux switch-client -t. Something like:
popup -E "tmux ls -F '#S' | sk | xargs tmux switch-client -t"
Make a keybinding for that, and here you go, you have a customized session picker!
On the other side, Zellij ships with its “session manager” — which, honestly looks terrible to me.

That thing is not something you have control on, and the Zellij CLI Actions³ won’t help you either.
Note: we are also in the right to ask ourselves why there is a list of commands (that you can bind to), and a list of CLI actions. The former is richer than the latter and it means that the commands you bind to your keys and the commands you run on the CLI do not share the same naming / they are not from the same set.
So you’re stuck with a “native” session picker, that could have been pushed to the users. Again, with tmux, it’s a one-liner, and it uses my favorite fuzzy picker.
Lately, I have decided to head over the Zellij news⁴ section to read about recent updates, and as mentioned at the very top of this article, I was not disappointed. The latest update features some new enhancements, which goes against the concept of minimalism and even against the initial scope of the project:
zpipe to pipe results into plugins. That’s interesting, but that’s also a new burden to maintain
because of using plugins in the first place, instead of going the composability route. I’m not convinced. With tmux,
I can simply send the result of a command to a specific session, window, pane. All of that just looks like a
justification for the extra complexity added by plugins. But at least it makes sense once you have decided to use
plugins?However, I don’t want to bash on Zellij. I think this very article I’m writing should be my reflection about why Zellij exists, why it’s not for me (anymore?) and why it could be for you. See, tmux is an old, battle-tested piece of software that will do exactly what it’s supposed to. If you want more, well, then you have to build it yourself. Because tmux composes well, it’s an enjoyable process for me, but it might not be for you. Maybe you don’t want to have to learn a fuzzy picker and the default, native session manager of Zellij will be a breeze for you. Maybe the file picker they have made will be of great help for you.
I could summarize my thoughts as follows:
You’ll find it here. The code is heavily commented and you can of course clone the repository and and run the executable with cargo.
I’ll post a more detailed blog post about the application I’m building with luminance right now later on.
Keep the vibe! :)
]]>There’s a package out there to do that, but it hasn’t been updated since 2008 and has a lot of dependencies I don’t like (InfixApplicative, OpenGL, OpenGLCheck, graphicsFormats, Codec-Image-Devil, and so on…). I like to keep things ultra simple and lightweight. So here we go. wavefront.
Currently, my package only builds up a pure value you can do whatever you want with. Upload it to the GPU, modify it, pretty print it, perform some physics on it. Whatever you want. The interface is not frozen yet and I need to perform some benchmarks to see if I have to improve the performance – the lexer is very simple and naive, I’d be amazed if the performance were that good yet.
As always, feel free to contribute, and keep in mind that the package will move quickly along the performance axis.
]]>I’ve been using interpolation (linear, bilinear, trilinear, B-splines, etc.) for a while now because of my demoscene productions. My spectra crate features some interpolation primitives and I’ve been wanting to move them out of spectra for a while.
Basically, my need is simple:
That’s a lot of things. For what it’s worth, I’ll just use as reference a library I wrote years ago in Haskell, smoothie, doing exactly that.
However, because I didn’t want to bloat the ecosystem, I had a look around to see whether I could simply drop my code and add a simple dependency instead of extracting things from spectra and creating, again, new crates. Here are the reasons why.
bsplineThis crate is about B-spline only and the interface is highly unsafe (panics if out of control points for instance). My current spectra code is already safer than that, so no reason to lose features here.
splineThis is… a… placeholder crate – i.e. it doesn’t have anything exposed. People do this to book a
crate name for later. I highly dislike that and people doing this should really be warned not to do
this, because they prevent others – who actually have some code to put there – from using the name.
Plus, for this spline crate, I had already have a look years ago. In years, the so-called “author”
hasn’t even uploaded anything and the name is just reserved… for nothing.
I highly suggest people from the Rust team to forbid people from doing this. This is just useless, childish and brings zero value to the community. Just remove that crate as no one has ever had the chance to ever depend on it.
trajectoryThis is an interesting crate, but it’s way too much specialized to me. It could be very interesting
to use it for camera and objects paths, but I also need splines for a lot of other situations, so
this is too much restrictive (you can see functions like acceleration, position, velocity that
works only for T: Float).
nbezA – looks like – very comprehensive Bezier crate, but doesn’t provide anything for all the other interpolation I need.
Looks like the crate I’m looking for doesn’t exist yet – oh yes it does: it’s spectra!, but I want a crate that does interpolation only.
I could be using some of the crates listed above, add them as a bunch of dependencies to a glue crate and just expose the whole thing. However, I’m not sure they will compose quite correctly and I might just end up re-coding the whole thing.
So the crate I’ll be building will be about interpolation. You will find step (constant)
interpolation, linear / bilinear / trilinear interpolation, cosine, cubice, Catmull-Rom and Bézier
spline interpolations. The crate will be as abstract as possible to enable developers to plug in
their own types. Because the spline name is already taken – duh! – I’ll use the splines name…
I’ll take my chance to try to take over
splinefirst, though. I really don’t like that kind of things in an ecosystem, it’s just unaesthetic.
Note²: Ok, I tried, and seems like it’s more complicated than I thought. I’ll just go with the
splinesname then.
So this blogpost serves as an introduction to splines. I added a few unit tests to start off with and a very few examples in the documentation. Most of the interface is sufficient for now but more complex needs might show up. If you have any, please shoot an issue or even better, open a PR!
Keep the vibes and happy hacking!
]]>impl block. An impl block is just a scope that introduces a
way to augment a type with methods – do not confuse impl blocks with trait impls, used to
implement a given trait.
The syntax is the following:
pub struct MyId(u32);
impl MyId {
/// A method that creates a null identifier.
pub fn nil() -> Self {
MyType(0)
}
/// A method that checks whether an ID is odd (why not?).
pub fn is_odd(&self) -> bool {
self.0 % 2 != 0
}
// some other methods…
}
This syntax gives you the dot notation and the static method call syntax:
let x = MyId::nil(); // static method call
println!("is it odd? {}", x.is_odd()); // dot notation
Most of the time, you’ll want impl blocks for:
A type variable is a variable that holds a type. People coming from languages like C++, Java, C#, Python, D, etc. are used to name those template parameters or type parameters.
pub struct List<T> {
// hidden
}
impl<T> List<T> {
// hidden
}
T here is a type variable, and you can see that you can have an impl block over List<T>. What
that means is that any declared method will have a hidden type Self = List<T>. If you declare a
static method, Self will be set as List<T> and if you declare a regular method, self, &self
or &mut self will have types – respectively – List<T>, &List<T> and &mut List<T>.
That sharing is even more pronounced when you start to deal with more complex data types and want to restrain their type variables with trait bounds. For instance, imagine that you want an ordered list:
pub struct OrderedList<T> {
// hidden
}
You could implement OrderedList<T> for any T, but it’s very likely that for a lot of methods,
you’ll need T to be comparable. So you can do this, for example:
impl<T> OrderedList<T> where T: PartialOrd {
// hidden
}
All the methods and static methods will have Self an alias to List<T> where T: PartialOrd. This
will drastically help you when writing methods that all require the trait bound.
Note that you can have several impl blocks for any kind of refactoring. For instance, if you
also want to have two methods that don’t need the trait bound, you can go and add another impl
block that looks like:
impl<T> OrderedList<T> {
pub fn new() -> Self {
// hidden
}
pub fn singleton(sole: T) -> Self {
// hidden
}
}
Now let’s try something a bit more complex. Let’s try this:
trait Foo<E> {
fn foo(&self, x: E);
}
impl<T, E> OrderedList<T> where T: Foo<E> {
fn invoke_foo(&self, e: E) {
for t in self {
t.foo(e);
}
}
}
This won’t work and rustc will complain that E is not contrained enough (it doesn’t appear in
Self, it’s only used in a trait bound on T). So what’s the problem?
You can test here
The problem with that last block is that E is not constrained on OrderedList<T>. If you
don’t know what it means, don’t freak out: I’m going to introduce every concepts you need to know.
Hang on, it’s gonna be a bit math–ish, but that is worth it.
In math, a surjection is a property about set morphisms. For a pair of sets C and D and a
morphism f : C -> D, we say that this morphism is surjective if all elements from D appear in
a f application. This is weirdly said, so let’s say it another way: if for all the elements
y in D, you can find at least one element x in C so that f(x) = y. Another way to
swallow that down if you’re still choking is by drawing the C and D sets as big bags with a few
elements in there. The morphism f applications are just lines from objects from C to D. If all
elements from D have at least one arriving arrow, the morphism is surjective.
We write surjection this way:
Ɐy ∈ D, ∃x ∈ C, f : C -> D / f(x) = y
And we read it this way:
Ɐy means for all y.∈ D means is an element of D. So Ɐy ∈ D reads for all y that is an element of D., often reads as juxtaposition.∃x means there exists x.f : C -> D is just the type definition of the f morphism./ is often used instead of , to delimit the equation and the hypothesis; read it as
so that.So this whole math stuff reads:
For all
yinD, there existsxinCso thatf(x) = y.
It’s quite a non-intuitive notation when you don’t know about it but you can see the concepts are pretty simple to understand.
You might ask, what is the point of knowing that in our case? Here, Foo is a type-level function.
Rust has decided to go with the mainstream way to note that (which is another topic and should be
the topic of another blog post, to be honest), so yeah, it’s a bit uglier than in Haskell, but it’s
a type-level function. You can picture it as Foo : E -> Foo<E>. You can easily see that if you
are writing the body of a function and that you have a bound like T: Foo<E> available, you know
that you have type T that implements Foo<E>, so there’s definitely one type to substitute E
with (otherwise your function cannot be called), but there could be several ones. This means that
traits are surjective.
Another way to see why it’s surjective: if I give you Foo<u32>, you know that there’s at least one
type implementing it, but you don’t know which one, since it’s ambiguous.
struct A;
struct B;
impl Foo<u32> for A {
// hidden
}
impl Foo<u32> for B {
// hidden
}
If I give you Foo<u32> and tell you “It’s in a bound position of a function”, you know that
if the function gets called, there’s at least one type implementing Foo<u32>… But which one is it?
A? B?
Injection is the reversed version of surjection. It tells you that all the elements from
C have a departing arrow (i.e. morphism application) ending in D and the elements in D
have zero or one arriving arrow. Another way to say it is that for all pairs of x and y
in C, f(a) = f(b) implies a = b, which means that you cannot have two inputs mapping to the
same result since if two applications of the morphism yield the same result, it means that the
inputs have to be the same.
Ɐ(x, y) ∈ C, f : C -> D / f(x) = f(y) => x = y
=> is the math notation to state implication.Two interesting properties of injections:
D (which is called the codomain of the morphism while C is its
domain) might not have any arriving arrow.The contrapositive is also interesting:
Ɐ(x, y) ∈ C, f : C -> D / x ≠ y => f(x) ≠ f(y)
It’s easy to imagine an injection in Rust. For instance, the function transforming a boolean into
an integer is an injection. Every value in the domain (bool) have an arrow into the codomain
(u8 for instance):
f(false) = 0.f(true) = 1.You can also see 0 has a single arriving arrow, and if you take it backwards, you end up on
false. Same thing happens for 1 and true. Finally, you can see that 3, 64 or 42, which
are definitely in the codomain, have no arriving arrow. We are dealing with an injection.
What’s interesting with our specific typesystem problem is that if we had injective traits, that
would mean that given Foo<T>, there’s only one type that can implement this trait. You would then
be able to take the bound backwards and recover the type, removing the ambiguity.
This is not correctly possible in Rust. If you’re interested, Haskell can do it via injective type families or functional dependencies, for instance.
For curiosity only, bijection is just the superposition of both the properties. If you have a morphism that is both surjective and injective, it is bijective, and any element in the domain gives you one element in the codomain and any element in the codomain gives you one element in the domain an element in the codomain must have exactly one arriving arrow.
trait Foo<E> {
fn foo(&self, x: E);
}
impl<T, E> OrderedList<T> where T: Foo<E> {
fn invoke_foo(&self, e: E) {
for t in self {
t.foo(e);
}
}
}
This is our initial problem. As you can see here, the impl block has two variables: T and E.
T here means that we’re implementing methods for all the Ts – i.e. Ɐ. However, we’re not
implementing it for all the E. We would like to state that we need a E to exist, which means
that there exists an E – i.e. ∃. The current syntax doesn’t support this.
However, what happens if we do this:
impl<T> OrderedList<T> {
fn invoke_foo<E>(&self, e: E) where T: Foo<E> {
for t in self {
t.foo(e);
}
}
}
Here, you can see that since we can provide E by the caller of the invoke_foo function, we don’t
require an injective bound: we narrow all the possible types to one provided by the caller of the
function.
You can test it by yourself here. The
Clonetrait was added so that we can actually loop with the input argument.
All of this made me think about why do we even use impl blocks. I mean, Haskell has been around
for roughly 30 years and we never seen the need for it arise. Rust has a very special way to express
typeclasses (Rust’s traits have an implicit type parameter, Self, which is a sort of limitation
that might get fixed in the future). If we forget about the dot notation, I truly think that I
would drop impl blocks for several reasons:
impl blocks so far.impl block in the documentation. This refactoring might be interesting for code readers and
writers but is, to me, truly a nightmare to people reading the documentation – it kind of makes
reading the documentation stateful, which is ridiculous. However, this point might be a very
interesting rustdoc feature request! ;)impl blocks because those
depend mostly on the functions / methods and I find it utterly stupid to create n blocks for
n functions. So I basically just spawn the most general form of impl block and put the
constraints next to the methods.impl trait implementors when reading the code from someone else.What would be really great would be to allow people to create function with a self argument
without impl block. You could still have them around for compatibility purposes and for people
who actually enjoy them, but to me those are a bit useless and boilerplate.
Again, I’m only talking about
implblocks.implfor trait are actually quite nice since you can see them and spot them easily (as theinstancekeyword in Haskell).
That’s all for today. Thank you for having read me. Keep the vibes, and write RFCs!
]]>See, most tools in Rust are wonderful:
rustc, the official compiler, is pretty easy to use and even though it’s an advanced piece of
software, it’s pretty unlikely you’ll have to use it directly (unless you’re doing something
very exotic, or working on it directly).cargo, by far the most appealing tool in the Rust ecosystem to me, is a Rust build system and
package manager. It downloads and checks your projects’ dependencies, using https://crates.io as
a default mirror for your crates (but you can
use other mirrors if you want and are brave enough,
even yours), (cross) compiles, runs library, binary and even documentation tests, etc.cargo also comes with a plugin system, allowing people to write cargo subcommands that can
be installed like with cargo install cargo-tree and run with cargo tree. Most common plugins
are:
bloat.watch.tree.clippy.fuzz.expand.outdated.rustup, used to select, switch, install and update Rust compilers, toolchains, support
cross-compilation and select targets, etc.Clearly, you can see we have lots of wonderful tools made by talented people and you can clearly feel how easy it is to start writing a crate, test it and publish it along with its documentation. It feels seamless. The documentation is a very important thing in any tech ecosystem for several reasons. And this blog is going to be about doc tooling and some specific parts of Rust.
Whatever the project you work on, you should must document your code. There are several situations – let’s call this the First Hypothesis:
If you write code that is intended to be released publicly, you must document every public symbols:
impl Default can be!).Now, if you are writing something that is private to a crate that you plan to release, think about people who will want to contribute to help you with the crate. They will have to get to read your code. They will have to go through the (sometimes painful) process of adapting to someone else’s way to write code. So do them a favor: document your code.
Finally, what about things you don’t plan to release? Like, for instance, a binary? The same rule actually applies: what happens if someone tries to contribute? What if you save the project aside for some weeks and get back to it afterwards?
Those three paragraphs just show you that the First Hypothesis was wrong: there are not several
situations. Only one: as soon as you write code, whatever whom it’s aimed at, just document it.
You’ll be thanked and you might even thank yourself in a near future for doing so. Documenting your
code enables you to run cargo doc --open and immediately start exploring. Exploring is when you
need to catch up with a project’s ideas, concepts, or whenever you join a team as a new job and need
to get used to the codebase.
I want to share my experience here: most people are bad at this – it’s not necessarily their faults, don’t blame them. Most teams I worked in were under high pressure, with business deadlines to meet – I’ve been there and still am. This is more about a political discussion to have with “the ones who give you such deadlines” but really, developing a project doesn’t stop at writing code and testing. Onboarding and discoverability should be considered of a massive importance, because it helps preventing the project from burying itself and getting too bloated for newcomers or even long-running team members to understand the actual heck is happening there. When someone new joins your team, you should help them to get accustomed to the codebase… but you also have to work. They should have leads or hints to get their feet wet. Several solutions exist to document that in kind of standardized ways:
README.md in all the projects your team has. This should be mandatory. Describe what
the project is about, how to build it as a developer, how to run it as a dev-ops. Describe the
architecture, etc.CONTRIBUTING.md to help onboarding newcomers with your team guidelines, conventions,
engineering processes, etc.README.md: write it!README.md of your team. This is a real plus: people
will just go to that documentation link to search for things “that have already been done by
the team” instead of reinventing the wheels – trust me, even the ones who wrote the features
might have forgotten writing them already.We’re now reaching the point of this blog entry: documentation can (should?) be used to explore what a project is about, how to use it, what are the public variants and invariants, etc. But there is however a problem in the current Rust ecosystem, preventing us from completely have a comfortable exploration of a crate or set of crates. A very, very important kind of public symbol is missing from the list of documented Rust symbols. Do you think you can figure out which one?
Have you ever worked with a customized crate? If you don’t know what I’m referring to, I’m thinking about crates that can be configured with features. For instance, as a good example of this, I’ll talk about a crate of mine: splines. The crate has several features you can set to achieve different effects:
"serialization": this feature will turn on the serde serialization code (both Serialize
and Deserialize) for all the public types exposed by splines."impl-cgmath": turn this on to have some cgmath’s types be usable directly within splines
by implementing the appropriate traits from splines."impl-nalgebra": same thing as "impl-cgmath" but for nalgebra."std": automatically enabled, you can disable it with default-features = false or
--no-default-features. When disabled, the code behaves compiled with no_std.All of this is currently documented in the README.md of the project and the top-level
documentation of the project (for instance, splines-0.2.3 has this paragraph about features and
what they do).
We have two problems here:
rustc and cargo!You can see that the situation is not really comfortable. I looked into the list of PRs and issues about that topic on rust-lang/rfcs and didn’t find anything. So I might start to write some ideas here and maybe, depending on the feedback, write a new RFC to fix that problem.
In order to know what modification we can do without introducing breaking changes, we need to have
a look at how features are defined. This is done in a Cargo.toml manifest, such as (from
splines):
[features]
default = ["std", "impl-cgmath"]
serialization = ["serde", "serde_derive"]
std = []
impl-cgmath = ["cgmath"]
impl-nalgebra = ["nalgebra"]
A feature is a key-value object where the key is the name of the feature and the value is a list of dependencies (crate names, crate cluster, a feature of another crate, etc. – more about that here).
We could document the features here. After all, that’s what Haskell does with cabal and flags:
Flag Debug
Description: Enable debug support
Default: False
So why not simply do the exact same in Rust within a Cargo.toml manifest? Like so:
[features]
default = ["std", "impl-cgmath"]
[features.serialization]
description = "Set to automatically derive Serialize and Deserialize from serde."
dependencies = ["serde", "serde_derive"]
[features.std]
description = "Compile with the standard library. Disable to compile with no_std."
dependencies = []
[features.impl-cgmath]
description = "Implement splines’ traits for some cgmath public types."
dependencies = ["cgmath"]
[features.impl-nalgebra]
description = "Implement splines’ traits for some nalgebra public types."
dependencies = ["nalgebra"]
In order to introduce this feature in a retro-compatible way, using the “old” syntax would just behave the same way but without a description, making them optional.
The whole purpose of this addition would be that the rustdoc team could assume the existence of an
optional description field on features and present them when browsing a crate’s documentation,
enriching the exploration and discovering experiences of developers. Instead of having to look at
both lib.rs and Cargo.toml to look for features, one could just simply go on https://docs.rs,
look for the crate they want to get features information et voila!
Some people might think of the consequences of features. Those are, after all, used for
conditional compilation. What that means is that part of the code might be hidden from the
documentation if it’s feature-gated and that the feature wasn’t set when generating the
documentation. An example with splines here.
You can see a few implementors of the Interpolate trait (some basic types and some from cgmath
because the "impl-cgmath" feature is enabled by default) but not the potential ones for
nalgebra, which is somehow misleading – people don’t know they can actually use nalgebra with
splines!
A workaround to this problem is to get a local copy of the documentation, generating it with the wished features. However, this will not fix the problem of the discoverability.
A solution to this could be to simply “ignore” the features while generating the documentation –
and only while generating it but annotate the documented symbols with the features. That would
enable something like this (generated with cargo doc --open --no-default-features and retouched
to mock the idea up):

If several features are required, they would just be presented as a list above the annotated item.
That’s all for me today. Please provide your feedback about that idea. If you like it, I’ll write an RFC because I really think it’s a missing thing in the documentation world of Rust.
Keep the vibes!
]]>On the 19th of March 2015, a law was introduced in France. That law was entitled “loi du renseignement” and was presented in regard of what happened in the Charlie Hebdo awful week. The main idea of the law is to put peole on wire so that terrorist activities could be spotted and dismantled.
That law will be voted and apply on the the 5th of May 2015.
Although such a law sounds great for counter-terrorism, it has several major issues people should be aware of.
Every people – in France and abroad as well of course – could be on wire. The French government might have access to whatever you do with your internet connection. Crypto-analysts working for the goverment will read dozens of thousands of messages from individuals. They’ll know where you go to and where you go from – think of your smartphone’s GPS capabilities. They’ll know which pictures you take, how much time you spend in your bathroom. They’ll even be able to read your e-mails if they want to.
Of course, most people “just don’t care”. “Why should I care that the government knows the litter brand I bought to my cat? I don’t give a damned heck as long as they catch bad people”. That’s a point, but that’s also being blind. Digital communication is not only about yourself. It’s about people. It’s about you and your friends. You, and your contacts. You can’t choose for them whether they’d like people to watch over their shoulders every now and then. If the government has access to your private data, it has access to your friends’ and contacts’ data as well. Not giving a damned heck is being selfish.
Then comes the issue with what the law is. It gives the government the right to spy on masses. They could even sell the data they collect. As a counter argument, the “Commission de contrôle des techniques de renseignement” – which French stands for “Control commission of intelligence techniques” – was created. That committe is there to watch the government and ensure it doesn’t go out of control and doesn’t violate people’s rights. The issue with that is that our prime minister has the right to ignore the committee’s decision. If the committee says “Wait a minute. You don’t have the right to gather those information without asking for M. Dupont’s approval”, the prime minister may answer back “Well, fuck off. I will and you can’t stop me”. And the sad truth is that… yeah, with that law, the prime minister and their team has the right to ignore the committee’s decision.
The committee then has no point. It just gives an opinion. Not a veto. What would happen if a terrorist hacked into your computer. Would you go to jail because the prime minister would have stated you were a terrorist? Damn me if I know.
French people will lose a very important right: the right to privacy. It’ll be sacrificied without your opinion for counter-terrorism, giving the government a power it shouldn’t have. You thought the NSA was/is wrong? Sure it is. But when the NSA watches over American and worldwide people, it is illegal whereas when the French government watches over French and worldwide people, it is legal. That makes the French government way worse than the NSA to me.
I think the first thing I’ll do will be to revoke my French VPS subscription to move it out of France, in a country in which people still have privacy. Keep your communication encrypted as much as possible (ssh and so on). And as a bad joke, don’t leave your camera in your bedroom. You might be spied on by Manuel Valls while having sex with your girlfriend.
]]>I’ve been facing an issue several times in my Rust lifetime experience and never ever have come up with a pragmatic solution to this problem yet. This problem comes up as follows:
As you can see, we have a situation here. Rust doesn’t allow to expose a trait without showing its internals. Imagine the following code:
pub fn a_cool_function<T>(id: usize, foo: T) where T: Foo {
// …
}
pub trait Foo: Sized {
type Cons: Sized;
fn compute(self, a: Self::Cons) -> Self;
}
impl Foo for String {
type Cons = char;
fn compute(mut self, a: Self::Cons) -> Self {
self.insert(0, a);
self
}
}
impl<T> Foo for VecDeque<T> {
type Cons = T;
fn compute(mut self, a: Self::Cons) -> Self {
self.push_front(a);
self
}
}
We want to export a_cool_function. That function accepts as second argument a type that must
implement the Foo trait. In order for the function not to leak private symbols, Foo then must
be public. However, we don’t want to expose its internals (the Cons associated type nor the
compute method). Why we would want to hide those? I have several points:
a_cool_function requires its second argument to
implement Foo. Not exposing the internals would then force people to use stock implementors
and will prevent them from providing new ones. This might be wanted for several reasons (unsafe
traits, performances, etc.).Cons required a bound on a very
specific trait that is there only because of your implementation details / choices (like a trait
from a dependency). That would leak that trait into the interface, which is not wanted.Currently, you can perfectly make a type public without exposing all its methods as public. Why not having the same power with traits?
There is a – non-ideal – solution to this problem:
#[doc(hidden)]on each items to hide from the trait. Items tagged with that annotation won’t show up in the documentation, but they will be definitely usable if a crafty developer reads the source. Not a very good solution to me, thus.
It’s been a while since I’m looking for a good RFC to introduce this in Rust. This is some needed jargon, so let’s explain a few terms first:
where clauses to constrain a type or a function.Currently, when you implement Display, you implement the trait. The fmt function you implement
is part of its trait definition. When you write a function like
fn show<T>(x: T) where T: Display, here, Display is not a trait: it’s a bound.
Bounds are interesting, because you cannot directly manipulate them. They only appear when
you constrain a function or type. You can combine them, though, with the + operator:
fn borrow<'a, T>(x: &'a T) -> MyBorrow<'a, T> where T: Display + 'a
This example shows you that lifetimes can be used as bounds as well.
The idea of this RFC is to make a clear distinction between Display as trait and Display as
bound so that it’s possible to use a trait only in bounds position and not implementation. One
major avantage of doing so is to bring completely new semantics to Rust: exposing a trait as
public so that people can pick types that implement the trait without exposing what the trait is
about. This brings a new rule to the game: it’s possible to create ad hoc polymorphism that doesn’t
leak its definition.
The idea is that we love types and we love our type systems. You might come across a situation in which you need to restrict the set of types that a function can use but in the same time, the implementation of the trait used to restrict the types is either unsafe, or complex, or depends on invariants of your crate. In my spectra crate, I have some traits that are currently public that leak rendering dependencies, which is something I really dislike.
As a prior art section, here’s the wanted feature in Haskell:
{-# LANGUAGE FlexibleInstances #-} -- don’t mind this
{-# LANGUAGE TypeFamilies #-} -- this either
module Lol (
Foo -- here, we state that we only export the typeclass, not its definition
) where
import Data.Text (Text, cons)
class Foo a where
type Cons a :: *
compute :: Cons a -> a -> a
instance Foo [a] where
type Cons [a] = a
compute = (:)
instance Foo Text where
type Cons Text = Char
compute = cons
Trying to use either the Cons associated type or compute function in a module importing Lol
will result in a compiler error, because those symbols won’t be accessible.
Currently, there is a weird privacy rule around traits. People not coming from Haskell might feel
okay about that, but I learned Rust years ago while being already fluent with Haskell and got
stunned at this (and I still have microseconds of “Wait, do I not need a pub here? Oh yeah, nah
nah nah.”) When you declare a trait as pub trait …, everything in its definition is automatically
pub as well.
This is so weird because everything else in Rust doesn’t work this way. For instance:
struct Bar; // here, Bar is not pub, so it’s private and scoped to the current module it’s defined in
pub(crate) struct Zoo; // not public either but can be used in other modules of the current crate
pub struct Point { // public
pub x: f32, // public
pub y: f32, // public
}
pub struct File { // public
inode: usize // private
}
enum Either<L, R> { // private
Left(L), // private
Right(R), // private
}
pub enum Choice<L, R> { // public
Left(L), // public (*)
Right(R), // public (*)
}
// (*): enums require their variants to be public if they’re public for obvious pattern-matching
// exhaustiveness reasons
pub struct Foo; // public
impl Foo {
fn quux(&self); // not public, only callable in the current module
pub(crate) fn crab_core_is_funny(self) -> Self; // not public but callable from within this crate
pub fn taylor_swift() -> Self; // public, callable from the crate and dependent crates
}
But:
trait PrivTrait { // private trait
fn method_a(); // private
fn method_b(); // ditto
pub fn method_c(); // compilation error and wouldn’t make sense anyway
}
pub trait PubTrait { // public trait
fn method_a(); // public, even without the pub privacy modifier!!!
pub(crate) fn method_b(); // won’t compile
pub fn method_c(); // won’t compile
}
To me, it would make much more sense for Rust to authorize this:
pub trait PubTrait { // public trait
fn method_a(); // private, only usable from this module
pub(crate) fn method_b(); // callable only from modules from this crate
pub fn method_c(); // public
}
However, I know, I know. Turning this feature on would break pretty much everyone’s code. That’s why I think – if people are interested by this feature – we should instead go for something like this:
trait PrivTrait { // private trait
fn method_a(); // private
pub(crate) fn method_b(); // compilation error: the trait is private
pub fn method_c(); // compilation error: the trait is private
}
pub trait PubTrait { // public trait
fn method_a(); // public (backward compatibility)
pub(crate) fn method_b(); // callable only from modules from this crate
pub fn method_c(); // public, akin not to use the pub modifier
priv fn method_d(); // private; only callable from this module
}
I’d like to point the reader to this issue. In its
foreword, @alexcrichton rightfully explains that removing the priv keyword was great because it
has yielded a rule ever since – quoting him:
“I think that this would really simplify public/private because there’s one and only one rule: private by default, public if you flag it.”
I feel uncomfortable with the current trait situation because that rule has been broken. This other issue pinpoints the problem from another look: traits shouldn’t set their items visibility based on their own visibilities. This yields weird and unexpected code rules and precludes interesting design semantics – the one I just described above.
I hope you liked that article and thoughts of mine. I might write a proper RFC if you peeps are hyped about the feature – I have personally been wanting this for a while but never found the time to write about it. Because I care – a lot – about that feature, even more than my previous RFC on features discoverability, please feel free to provide constructive criticism, especially regarding breaking-changes issues.
Keep the vibes!
]]>Disclaimer: this article expects some sort of fluency with Zig to read smoothly. However, the examples and snippets will be explained for the uninitiated ones.
Zig has many interesting features and safeties. I won’t cover them up here, it’s not the aim of the article, and I’m sure you’ll find that information on your own — there are plenty of articles talking about how well designed the language is.
No, instead, I want to talk about memory safety and static checks. And actually, it’s even more than memory safety. It’s more about correctness. Instead of taking the — traditional and boring — example of using an allocator (people using Rust, Python, C++, Java, etc. wouldn’t get the problem because they do not have a direct access to allocators — I want to show that the problem exists not only for memory allocations, but for a much more general category: resources. So let’s take an example:
const Storage = struct {
// … hidden
const Self = @This();
pub fn create_resource(self: *Self) StorageError!Handle {
// … hidden
}
pub fn destroy_resource(self: *Self, handle: Handle) StorageError!void {
// … hidden
}
};
Okay, so that is basically a Storage type with create_resource and
destroy_resource methods. The former creates a new resource in the Storage
and returns a Handle to it. The second one destroys a resource in a Storage
providing its Handle.
You would use it like so:
// …
const h = try storage.create_resource();
// do something with the storage …
// … then later, at some point:
try storage.destroy_resource(h);
It’s not important to use
deferhere; the problem would be the same. Also,deferwill not work on fallible methods.
Now, what happens if you forget to call destroy_resource? Well, your code
still compiles, while it’s not correct. What happens if you call
destroy_resource twice with the same pointer? Probably something bad, but it
still compiles.
The rest of the article assumes that we want to solve this problem at compile-time, because yes, Zig will catch the problem at runtime, probably (eventually?) resulting in a resource leak.
To solve this problem, we historically used — mainly — two approaches:
The latter option is interesting, because it’s actually composed of two different features:
If we take a bit of hindsight, the drop part is actually not that good, because it hides some implicitly called code from the programmer. It makes it harder to think in terms of when some code will be called — is this value of a type that implements a drop interface? if so, where does it go out of scope? etc.
Moreover, the real problem is not really to automatically call the cleanup code. The real problem is to be sure that the cleanup code is called. Eh, the nuance is subtle, but I think it makes all the difference.
What we really want to do is to be sure that the user call destroy_resource.
We should not need destructors for that. Let’s continue with our example:
const handle = try storage.create_resource();
The only way we can emit a compiler error here — because handle is not
destroyed! — is to recognize that handle has some special – compilation-only —
properties that need to be passed to the destroy_resource. ATS has some
answer to this — careful if you click the link; it’s a pretty complex language!
That answer is called Programing with Theorem-Proving (PwTP). My idea here is
not to take all of ATS but just the part that is interesting to our problem:
proofs.
A proof is a compile-time (comptime) value that is returned by a function,
typically during resource creation (allocation, opening files, etc.) and that is
linear. A linear value is a value that must be consumed at least once and at
most once — it must be consumed exactly once! Consider this:
fn do_something(x: Foo) void {
// …
}
const x = try alloc.create(Foo);
do_something(x);
If this program states that x is linear, after we pass the value to
do_something, x is not available anymore. If you have done some Rust, you
should be familiar with the concept. In Rust, we say that x was moved into the
function.
Back to the proof thing. We can simplify ATS proofs and create a simpler proof system by doing the following:
I know, that’s a lot of new ideas. I’ll introduce the syntax I have on my mind to explain a bit more what I think. Let’s start with a regular alloc/destroy wrapper and then enhance it:
fn create(alloc: Allocator, comptime T: type) Allocator.Error!*T {
const ptr = try alloc.create(T);
return ptr;
}
fn destroy(alloc: Allocator, ptr: anytype) void {
alloc.destroy(ptr);
}
Nothing fancy with this base code. Now, let’s introduce proof marking. The idea is that we want that not to be in the way too much. The syntax of a proof mark goes as follows:
| [a, b, …, e]#tag.| #tag.The | … syntax marks a type with a proof.
[a, b, …, e] is a list of tying arguments, which ties the proof to those
values. If omitted (hence using the second form), all of the function arguments
are used as tying the proof.
The #tag syntax declares a tag symbol in the current Zig file. That tag along
with the tying variables uniquely identifies the proof.
Proofs can only be created in return-position of a function. Let’s mark our
create function with a proof:
fn create(alloc: Allocator, comptime T: type) Allocator.Error!*T | #create {
const ptr = try alloc.create(T);
return ptr;
}
As you can see, proofs only live at compilation time, and the implementation
has not changed. | #create marks the return value of create with a proof
which is basically [alloc, comptime T]#create. Now, let’s create something:
test "linear allocations" {
const alloc = std.testing.allocator;
const ptr = try create(alloc, u32);
}
The type of ptr is *u32 | [alloc, u32]#create. Here, alloc is our testing
allocator, and thus we tie the ptr variable to the alloc variable and u32.
What we have here is what I would call a dangling proof. ptr type is still
marked by a proof, and this the compiler should emit an error stating that a
dangling proof is left to be consumed. To solve the problem, we must consume the
proof. Let’s modify destroy:
fn destroy(alloc: Allocator, ptr: anytype | [alloc, @typeInfo(@TypeOf(ptr)).Pointer.child]#create) void {
alloc.destroy(ptr);
}
Okay, that’s a lot. Let’s scan through the signature. We can see that ptr has
a proof-marked type. The proof ties alloc and, basicalyl, the type of the
pointee — because Zig’s comptime is so awesome, we can actually do all of that
at compile-time!
If you think this is too verbose, you can simply pass the type yourself:
fn destroy2(alloc: Allocator, comptime T: type, ptr: *T | [alloc, comptime T]#create) void {
// …
}
We can then properly consume our linear ptr:
test "linear allocations" {
const alloc = std.testing.allocator;
const ptr = try create(alloc, u32);
// 1. either
destroy(alloc, ptr);
// 2. or this
destroy2(alloc, u32, ptr);
}
If you have noticed, marking is done on the error union:
fn create(alloc: Allocator, comptime T: type) Allocator.Error!*T | #create {
And the question is: what happens if we return an error? I think that an error
would invalidate the proof. However, if you don’t call try, the type is:
test "linear allocations" {
const alloc = std.testing.allocator;
const ptr: Allocator.Error!*T | [alloc, u32]#create = create(alloc, u32);
}
If ptr is actually an Allocator.Error, the proof is lost and considered
consumed.
Such a change in the language would have tons of impacts, and because of that, I
think that nothing in the standard library should use the feature, to prevent
breaking pretty much everyone — the Allocator interface is at the heart of
the language and its ecosystem. However, I think that having the feature
available would allow people to use it in their own wrappers, and as mentioned
before, not only for memory allocation. You can easily see how such a feature
would be useful for files, database connections, and more.
I know that the Zig language will probably never accept a proposal like this,
but given comptime that is already there, I think it’s super close of actually
being able to provide PwTP.
Anyway, that was my ideas, and I will probably write more about Zig in the future.
Cheers!
]]>Anyway, among the features that Zig needs (and that we don’t really need in a pure Rust environment) are
defer and errdefer. It’s actually pretty simple to understand what they do:
defer <stuff> will execute <stuff> when control flow leaves the current scope, whether normally (end of block),
or abruptly (early-return; try). <stuff> can be a single statement, or a block.errdefer <stuff> will execute <stuff> when control flow leaves on an error.defer and errdefer in ZigYou usually use defer to deallocate resources after you are done with. For instance:
const ptr = try allocator.alloc(u8, len);
defer allocator.free(ptr);
At the end of the block or if an error occurs afterwards, ptr will be deallocated. This already creates a
bunch of issues you have to be concerned with:
ptr cannot escape the current block/function, because at the end of the block, allocater.free(ptr) will
be called. Zig has no way to broadcast this contract, so it’s on the programmer’s to ensure they don’t use
the pointer after being freed.Sometimes, you might want to allocate some fields to return an owned version of them, such as:
const field1 = try allocator.create(Field1);
const field2 = try allocator.create(Field2);
// …
return .{ .field1 = field1, .field2 = field2 };
You do not want to defer-free them, because that’s on the caller to do so properly (imagine a constructor function,
for instance). However, you might have spotted the problem here: what happens if the allocation of field2
fails? We leak field1. So you would need to do something like this:
const field1 = try allocator.create(Field1);
const field2 = allocator.create(Field2) catch |err| {
allocator.destroy(field1);
return err; // pass-through
};
// …
This is pretty annoying, so Zig has a solution: errdefer. It executes its right-side statement only in case of errors.
So you can write the same code like this:
const field1 = try allocator.create(Field1);
errdefer allocator.destroy(field1);
const field2 = try allocator.create(Field2);
Those two keywords exist in Zig to make resource management easier. But what about Rust?
Rust doesn’t need defer nor errdefer because it has the concept of dropping automatically — Drop, also known
as destructors. How does it work? Contrary to the value-based approach of Zig, dropping in Rust is type-based: a type
that implements Drop automatically gets its values dropped by drop glue code at the appropriate place in the code.
You can picture this as inserting the code defer and errdefer would put for you, but automatically. That prevents
forgetting calling destructors:
let name = String::from("Chuck Norris");
At the end of the block name is declared in, name will be cleaned up by calling its Drop::drop() implementation,
and eventually deallocated as part of its drop logic.
There are interesting consequences of this design:
Drop::drop(), as the compiler inserts that call at the right places for you.Drop glue code by using std::mem::leak(). That has some usecases, but should be avoided in 99,9999% for
obvious reasons.An interesting aspect of Drop is that if you have several ways of implementing the cleanup logic for a given value,
you can create as many types as logic of dropping you can think of, and move the value in the appropriate type wrapper
to select the drop logic.
That last point led me to a funny idea: we can actually implement defer and errdefer in Rust! Why you would want to
is open to discussions, but among the main reasons:
defer, as you
might need to perform the cleanup logic in a single function. Introducing a type just for that function might look a
bit overkill, so defer could be nice there.defer in RustImplementing defer in Rust is pretty straight-forward. Remember what Zig does: it actually inserts code that runs at
the right place for you — Zig people keep stating that everything is explicit in Zig, but it’s not completely true,
since defer and errdefer statements will run at various places that the compiler will insert at; it’s not apparent
and thus is very similar to Drop in that regard; you just need to manually ask the compiler to do so.
Let’s look at the Zig example again:
const ptr = try allocator.alloc(u8, len);
defer allocator.free(ptr);
The defer will call allocator.free(ptr) when we leaves the current block, which is akin as having a value
dropped at the end of the current scope in Rust. We can do that by introducing a simple type wrapper (3):
struct Defer<F>
where
F: FnMut(),
{
drop: F,
}
impl<F> Drop for Defer<F>
where
F: FnMut(),
{
fn drop(&mut self) {
(self.drop)()
}
}
We can then introduce a simple way to construct such a value in the scope of the call site:
macro_rules! defer {
($($item:tt)*) => {
let _defer = Defer {
drop: || { $($item)* }
};
}
}
Why the
let _defer? Because Rust, by default, is smart enough to realize that we won’t be using the result, and will likely immediately deallocate it without waiting for the end of the scope. By using an ignore-binding like that, we force Rust to drop the value at the end of the scope.
Using this is easy:
pub fn main() {
let mut counter = std::sync::atomic::AtomicU32::new(0);
defer! {
println!("should be called last: {counter:?}");
counter.store(1, std::sync::atomic::Ordering::Relaxed);
}
{
defer! {
println!("should be called first: {counter:?}");
counter.store(2, std::sync::atomic::Ordering::Relaxed);
}
}
println!("should be called second: {counter:?}");
}
The first defer! will be called last — at the end of main, and the second one will be called when the enclosing
scope exits, which will then make it the first one to execute. The full output will be:
should be called first: 0
should be called second: 2
should be called last: 2
Now, how would we take care of implementing errdefer in Rust, though?
errdefer in Rusterrdefer is trickier, because it requires the compiler to insert code when an error occurs. But Rust doesn’t have
the concept of error baked-in. Result<A, E> and Option<A> can be used for error handling. Actually, anything
implementing the std::ops::Try trait.
Besides checking whether the thread panicked during a Drop implementation, we cannot really detect and run code when
an error occurred. We have to manually introduce the concept. The idea is actually a combination of two principles:
Drop will call code when it’s dropped (end of scope, like for defer).The second idea is key. Because we eventually need to return something, we will have to wrap our result in an Ok-like
enum variant. The whole idea goes like so:
errdefer: ErrDefer<F>,
which is similar to Defer<F>, but this time we want it to be explicitly named and used by the user.ErrResult<A, E>, which is a type wrapper over a Result<A, E>, but can only be
constructed from an ErrDefer<F>.errdefer.ok(value), which wraps value in
a ErrResult<A, E>. This call is responsible for ensuring the error closure will not run when the ErrDefer<F> is
dropped.errdefer.err(err_value). This will simply wrap
err_value in an ErrResult<A, E>, and will let the ErrDefer<F> be dropped, calling the closure.try function calls returning ErrResult<A, E>. More on that below.Let’s start with ErrDefer<F>:
struct ErrDefer<F>
where
F: FnMut(),
{
err: Option<F>,
}
impl<F> Drop for ErrDefer<F>
where
F: FnMut(),
{
fn drop(&mut self) {
if let Some(mut f) = self.err.take() {
f();
}
}
}
Here, we wrap the closure in an Option<F> so that we can specifically decide whether we want to call it in Drop if
it’s Some(f), or just drop the function without calling it with None. That decision is taken later when returning
values with ErrResult<A, E>, which we can have a look at right now:
struct ErrResult<A, E> {
result: Result<A, E>,
}
impl<A, E> ErrResult<A, E> {
fn into_result(self) -> Result<A, E> {
self.result
}
}
Nothing fancy, just a type wrapper over a Result<A, E>. Now let’s see how we can build it:
impl<F> ErrDefer<F>
where
F: FnMut(),
{
fn new(f: F) -> Self {
Self { err: Some(f) }
}
fn ok<A, E>(mut self, success: A) -> ErrResult<A, E> {
self.err = None;
ErrResult { result: Ok(success) }
}
fn err<A, E>(self, e: E) -> ErrResult<A, E> {
ErrResult { result: Err(e) }
}
}
If you look at ErrDefer::ok(), you can see that before wrapping the success: A into an ErrResult<A, E>, we set
the Option<F> closure in ErrDefer<F> to None. That will cause its Drop implementation not to call the closure.
However, if you look at ErrDefer::err(), can see that the closure is not dropped, and as such, when the ErrDefer<F>
will be dropped, it will call its carried error closure.
You will also see the ErrDefer::new() method, which will not be practical to use, as we will have to wrap our code in
a closure. We will use the same idea as with Defer<F> here and use a macro:
macro_rules! errdefer {
($($block:tt)*) => {
ErrDefer::new(|| { $($block)* })
}
}
We can then write code like this:
fn success() -> ErrResult<i32, &'static str> {
let result = errdefer! {
println!("will not be called");
};
result.ok(10)
}
fn fail() -> ErrResult<i32, &'static str> {
let result = errdefer! {
println!("an error occurred");
};
result.err("nope")
}
This is great, but not very useful. Indeed, something that would be more useful would be to able to compose functions
that might fail like we do with the try keyword in Zig; or with the ? operator in Rust. The key here is that
ErrResult<A, E> type. As with the previous problem where the compiler was not aware of error paths, the compiler here
cannot help us understand that a function call returning ErrResult<A, E> can actually make the current function fail,
so we need to help it:
macro_rules! errtry {
($errdefer:ident, $res:expr) => {
match $res.result {
Ok(success) => success,
Err(err) => return $errdefer.err(err),
}
};
}
This macro is not production-ready and just there to demonstrate the idea, but it still holds: it inspects the content
of the ErrResult<A, E> (actually, anything that has a err() method as first argument, and result: Result<_, _> as
second’s field argument, but this is not important), and convert it to either A, or wrap the E error into the
current ErrDefer<A, E>.
You can use this like so:
fn transitive() -> ErrResult<i32, &'static str> {
let result = errdefer! {
println!("something failed deeper the stack…");
};
let failed = errtry!(result, fail());
result.ok(failed * 10)
}
Combining and calling all of our previous functions:
fn main() {
let a = success().into_result();
println!("a = {a:?}");
let b = fail().into_result();
println!("b = {b:?}");
let c = transitive().into_result();
println!("c = {c:?}");
}
We can this output:
a = Ok(10)
an error occurred
b = Err("nope")
an error occurred
something failed deeper the stack…
c = Err("nope")
Something interesting to notice here is that errtry! works only for a function, which is also the case in Rust with
the ? operator. This is similar to Zig, but there is a slight difference with my current implementation: Zig will
evaluate the exit scope value, meaning that if we wanted to have the same result in Rust, we would need to exit
all scopes (blocks) manually with a errdefer.ok(value) to have the same effect. Another idea would be to have the
function stored as F directly, in a bool — defaulted to false — set to true when errdefer.err(_) is called,
allowing inspecting that value in Drop::drop() to call if needed.
Now, let’s have a rationale.
The honest and short answer is: no, probably not. However, it can be useful in some circumstances, as mentioned earlier,
especially when writing code to interface with C. Sometimes, you might need to have that kind of cleanup that requires
defer and errdefer, and having those macros can massively help you.
On the other side, you could also just go for a type wrapper that implements Drop for you, and you have the same
logic that can be run for both situations automatically (defer / errdefer) without thinking too much about it. This
whole blog entry was an experiment I wanted to make to show that type-based Drop — proper destructors — are more
general and reliable than defer and errdefer — you can’t forget to call them.
With hindsight, I find it a bit a pity to have the Zig compiler infrastructure be able to inject code at scope exit / on
error, but require the user to do it. I know Zig people would tell you that everything should be explicit, which I
think is both a lie and not a strong argument. I think a better argument would be to state that without move semantics,
which is pretty hard to implement correctly (to my knowledge, only Rust does it correctly; more on that in the next
paragraph), Zig cannot have destructors, otherwise, how do you make a difference between a function that returns an
ArrayList(T) — and thus must not deallocate it! — and a function that allocates one, performs some stuff with it, and
wants to get rid of it upon exit? This kind of questioning is why I think people are wrong about defer and
errdefer: they are not a feature per-se, but a required mean because Zig doesn’t have ownership and move
semantics. That strengthened my opinion about the fact that holistic language archictecture moves problems from
user-land into compiler / language. The lack of a static construct forces you to introduce keywords and solutions that
wouldn’t be required if you had the construct.
And why do I think only Rust implements move semantics correctly? The only other language that I know of which has move semantics is C++, and the way this is implemented in C++ is completely flawed by constructors. Because bindings cannot be invalidated in C++; when a value is constructed, it can have its content moved-out from another value. You then need to just copy those fields (bit-wise copy), but manually invalidate the internals of the other value. That means that the other value’s destructor will still be called, and it will have to inspect its state to realize its content has been moved. I hate this design. It’s overly complicated, and super error-prone.
As always, I hope you enjoyed this small experiment and that it gave you ideas.
Keep the vibes!
]]>This was written in a single throw in less than one hour. I needed to make it that way, because otherwise it would have ended up in one of my daily journals. But for some reason, I wanted this to be public. I wrote that in my Kakoune editor, and no LLM was involved.
Waking up. Eating. Having a nice specialty hand-made espresso. Working. Practicing sport. Socializing. Gaming. Enjoying hobbies. Playing an instrument. Going to bed. Repeat.
While can agree that this is the main life frame of many people out there, every part of that scheme might be slightly different:
Among all of those things, there is one thing that has suffered lately on my side: the hobbies part, especially my spare-time projects.
I have been running many FOSS projects on my free time. The biggest one of the current moment is definitly kak-tree-sitter (KTS). It has been mature for a while now and used by many people in the Kakoune ecosystem. But besides that, I struggle to find the motivation to move on with the rest of my projects. And I do think I need to talk about it.
Lately, things have been a bit complex to me. I don’t want to leak information about both my professional nor private life, but let’s say that I had better years than 2025 and 2026. So finding the motivation to get back at enjoying my spare-time projects is hard. I discussed that with a couple people on IRC, colleagues, friends. The only thing that I know for sure is that forcing myself to it is not going to be productive. So I try to analyze why I feel that way, and I start to see pattern.
I do believe that I am mentally exhausted. Work is complex, we have many dynamic projects changing all the time, and it’s been years I’ve known that it’s much harder to work on my own stuff after a long day of hard work; that’s no secret. However, I do realize that it worsened lately, and the main reason is manifold. Let’s say that I spent way too much time judging myself and putting a worth / a value of my person instead of my work through external lenses — and most of the time, it’s just plain wrong. I do not know to give too much details, but let’s say that it took me a long time to realize that, sometimes, I should stop listening to what people say about me, and just focus on feeling better and enjoying what I’ve always enjoyed.
I have always loved writing software. I have started the design of a systems programming language because I started to be dissatisfied with Rust — and after testing Zig numerous times (seem my blog entries on the matter), I stated that no language exists that solves all the problems I would like to be solved at the language level. This is exciting. But it’s also exhausting. However, I take it slow, and I also decided to stop blaming me for feeling bad:
See the pattern? Good. Because it took me months, if not years, to actually see it. I have been organizing my whole life around the concept of yield, around productivity. I do know now that a human being cannot withstand such a high pressure of constant output. I completely miscalculated an important aspect of my own personality: I, too, require breaks, from time to time. People at the wakepark know me for being someone who can tank in and fight through 4h of constant wakeboardings with barely no pause, no break, just pure anaerobic exercises until I can’t barely breath anymore. I am like that. I spent my whole youth as a race swimmer, where we were used to almost throw up after each training session because it was that intense. Yes, it might sound too much for many. Yes, it is a way of living with giant ambitions. And yes, it has started to consume me.
Lately, after landing amazing tricks on my wakeboard; after landing a nice design at work or on my spare-time, my only reaction was either: it’s luck, or: yeah it’s normal. But after a single mistake? A single fall on a objectively hard wakeboard trick? On a design that doesn’t work the way I initially intended at work? I feel worthless. I feel shitty. I blur the value of the person I am with the failures I go through, and completely disregard any achievement.
This is toxic, and I do believe it has had a really big impact on my mood in the past few months. Not getting the recognition for my (important, cross-team, wide impact) work. Being on the neurodivergent spectrum — I am hypersensitive to others, with a « constant monitoring » kind of awareness of my environment, leading me to situations with friends I judged highly unfair while they completely missed the point in the first place and probably even forgot about it while I still relive the memories daily.
And then, there is the feedback I get from FOSS. A written feedback. People sent me emails. People talked to me on IRC. On Discord. I can’t deny all the amazing feedback I received about everything I’ve been doing. And sometimes, I realize how lost I can go and forget that most of the contributions I’ve been making since I started coding when I was 11 have been a good impact. Why would local mistakes or failures erase and replace all of that? Why is it so exhausting?
I decided to do some introspection regarding all of this after noticing that Wez has been away from its wezterm community. I do not know anything about Wez, what they are going through, whether it’s still happening, but I can relate so much, and I really hope they get better, just the same way I have started to feel better by just acknowledging the situation and just, you know, forced myself to realize that I am not a shitty developer, and that I should probably start listening people a bit less, and focus more on having fun. Go back to my demoscene days where I didn’t ask for feedback before doing something. I was just, doing things. And I think too much feedback leads to burnout.
Keep the vibes.
]]>The CHANGELOG is available to read but one of the most important change was the refactoring of
the pipeline system and of the overall API to make it less confusing. For instance, the
Framebuffer::default is now renamed Framebuffer::back_buffer. Another example is the face
culling, that was in pre-0.27 enabled by default (causing people who don’t now about it to be
confused because of a black screen). This is now disabled by default; hence less confusion.
The pipeline system types are now easier to use. For instance, redundancy was removed with the
various Bound* objects. When you want a bound buffer of f32 for instance, you just have to
use the type BoundBuffer<'a, f32> instead of the pre-0.27 BoundBuffer<'a, Buffer<f32>>. That is
a small change but it will eventually make working with luminance more comfortable.
Another change that I think is beneficial is the new TessSlice type – that replaces the pre-0.27
TessRender. This type represents a GPU tessellation slice. Such a slice can be used to render
part of a tessellation. What’s great is that the legacy methods to build such slices
(TessRender::one_whole for instance) have a new, more generalized way to do it:
TessSliceIndex<Idx>. This trait gives you a slice method which argument has type Idx. Several
implementors exist:
impl TessSliceIndex<Range<usize>> for Tess.impl TessSliceIndex<RangeFrom<usize>> for Tess.impl TessSliceIndex<RangeTo<usize>> for Tess.impl TessSliceIndex<RangeFull<usize>> for Tess.They all give you a TessSlice to work with (with the appropriate lifetime). The four
implementations listed above can be invoked with:
tess.slice(0..10).tess.slice(0..).tess.slice(..10).tess.slice(..).For those who wonder:
Why have you not implemented Index instead?
The current Index trait doesn’t give you enough power to index a value by returning something else than a direct reference. Everything is summed up in the RFC I wrote to fix Index.
Feel free to have a look at the CHANGELOG for further information.
One of the most important feature people have asked is adding examples. The online documentation is not enough – even if I spent lots of hours enhancing it. So I added several examples:
You can find them in this place.
Please do contribute if you thing something is missing, either by opening an issue or by opening a PR. Any kind of contribution is highly welcomed!
]]>I pushed a pull request to Edward Kmett’s either package
to implement two functions some guys was complaining not to find: flipEither :: Either e a -> Either a e
and flipEitherT :: EitherT e m a -> EitherT a m e.
When implementing the functions, I wondered: “Hey, flipping stuff is a pretty common operation. Don’t we have an abstraction for that yet?”. I haven’t found any.
I decided to make a little typeclass to see what it’d be.
class Swap s where
swap :: s a b -> s b a
instance Swap (,) where
swap (a,b) = (b,a)
instance Swap Either where
swap = flipEither
-- let’s go wild and fooled
instance Swap Map where
swap = fromList . fmap swap . toList
If you think that’s handy, I’ll write a little package with default instances to make it live.
Happy hacking folks!
]]>TL;DR: this is a bit ranty, and is not necessarily well organized. You will be surprised that I have not put that text through an AI agent to review and rework it. You have been warned: you are reading something that was entirely written by a damn human being.
This blog post contains an important announcement that you should still read, though.
I had anticipated writing such a blog article for a long time by now (months? years?). Back in June 2024, I wrote a blog post about switching to an email-based workflow, and the will to switch to a decentralized life. I’ve been having several issues with many different services I use, including GitHub. The idea that Microsoft bought it started to itch me, for the simple reason that it was obvious what microsoft had been doing all along.
Remember this?
If you happen to be using freely a service owned by a company, you are very likely the product.
So back in the days, I decided to start moving my main repositories — i.e. the ones I actively maintain — as soon as possible, so that the new work process move to platforms I believe more in (SourceHut), and kept archives of the existing version of the projects on GitHub.
As time passed, my opinion on how I should run my spare-time projects matured. We’ve seen the advent of so-called « AI », which is a term I despise. It means absolutely nothing and is just a marketing buzzword, but also a massive and global engineering desillusion that is going to cause tons of harm mid- and long- terms. What do you think the developers of tomorrow will look like, not having a single clue about what a stack corruption means, or tail-recursion, or even simply how to think by themselves, being creative, where all they will have learned is to reach for their Claude prompt or Copilot 3000. AI models were supposed to be programmers assistants, not the other way around.
People won’t really thinking about how to solve a problem. They will ask the AI to solve the problem for them, and have to validate the solution. This is currently possible because most of the programmers out there actually have the required skills to understand and check the code (most of the time terrible) that is spit out by Cursor. At some point, those programmers will start to actually get taught by AI, lacking the foundational skills to verify the code. I strongly believe that not coming up with the actual implementation is going to create a generation of developers that will lack the creativity, resilience and methodologies required to be a good programmer, but also to just being able to read and understand it in the first place.
This is just one side of the issue.
I will not even mention the problem of energy, it’s well documented (i.e. AI sucks hard), and instead want to talk about two things.
The AI ecosystem gives little regard to copylefts and actually contributing in a collaborative way with the rest of the world. I have shared my thoughts on that several times, but it’s a global violation to millions of codebases (ripping off free and open-source code in order to train a company-owned, privileged, technology that users have to pay for, one way or another), while evaporating lakes. I am not okay with my code being used to train proprietary software, code that is part of profitable services that no one in the free and open-source world can possibly can even dream to replicate due to obvious… budget differences. People used to stay that companies using free and open-source code should provide a bit of money to the people actually maintaining that software on their spare-time; it’s code they rely on, that they didn’t have to write; that they don’t have to maintain. And yet, now, most people seem unbothered with the idea of companies absorbing the whole Internet to train their services that they will charge you for. WTF?!
The AI ecosystem is so aggressive that it causes trustworthy third parties
to experience DDoS and other various kinds of issues, which is absolutely disgusting. That led to people
coming up with solutions like AI Labyrinth or human-detection
algorithm when accessing forges. That is not a solution, because such platforms actually have to spend CPU
resources to fight against aggressive AI crawlers. If you are implementing a crawler that doesn’t respect
robots.txt or similar, you are part of the problem.
And then, there was this tweet from Thomas Dohmke — former GitHub CEO:
The evidence is clear: Either you embrace AI, or get out of this career.
This is like spitting directly to the faces of millions of developers worldwide. Of course, coming from a guy whose career depends on people actually following and paying for his AI development model, this is at least hilarious, yet disrespectful for people who, actually, do build software instead of babysitting a drunk completer that understands nothing about your project and keeps wasting everyone’s energy asking to review his triangle drawing while it actually drew a circle.
That so-called evidence is based on 22 engineers who use AI in their day to day work. 22. Twenty-two. Vingt-deux. If that is evidence for him, and that we should trust his judgement, then I officially announce that I believe FSM will be president of the world by 2050 — and I am almost 100% sure you can find more people believing my claim than his.
I do believe that we need to take a bit of hindsight on technology, and the place where we want to stand. From my perspective, I have read that AI makes us dumb. I do think that at some point, where we will need to come up with ideas to new problems, AI will come short. I — probably like many others — had to deal with Cursor comments at work on PRs I opened for my colleagues to review, just to realize that the Cursor AI left a dumb comment about my code, proving that 1. it doesn’t understand the project and 2. even the suggestion it does is wrong, without really looking into the context of the project. This is just a way to make us lose and waste time at work, but we are using AI.
I do think AI agents are tools, and my opinion is that they are not mature tools (yet?) that I do not want to have to interact with (yet?). Woah, I don’t use AI to review the emails I send! Damn, I don’t use AI to write my blog articles! Holy shit, I don’t try to ask an AI what it thinks about the code I write! And you know what? Why would I? Why would I ask an AI to write a speech to describe a project I work on instead of actually being a human being and sitting at my desk, and activiting the actual and real neurons I still have in my brain? We have been having AI for a long time: a good compiler is a form of AI. But we have always used AI dedicated to solving a very specific problem. What those new AI agents are doing is trying to replace human thinking, with hyper advanced auto-completer. Yeah, I’ll pass.
I started last year to move my projects off GitHub to SourceHut. I want to use this blog article to officially say that I have started to move the rest of my projects off GitHub, and that I will not let archived repositories on GitHub. Of course, I am not doing that without first checking package managers and any dependent systems (I’m thinking Hackage for Haskell packages, and crates.io for Rust). So it will take a bit of time — I have actually quite a lot of source projects! — but I will eventually reach a point where none of the repositories I have on GitHub will be source projects.
If you depend on any of my GitHub projects, feel free to switch to the SourceHut tree instead.
As always, keep the vibes, and goodbye GitHub.
]]>spectra is a crate I’ve been maintaining for a few months / years now. It’s a crate that I mainly use for demoscene productions (I released two with it, Céleri Rémoulade and Outline Invitation) but I also use it to play around and experiment new rendering, animation and video game techniques.
The crate features a few things that I enjoy daily. Among them:
The last thing I’ve been working on is being productive. That might seem counter-intuitive, but when you start building “frameworks” and “engines”, you actually end up writing a lot of code for “the beauty and power of it” and lose focus on what’s important: releasing things. I know that and looked for what I could enhance to augment my productivity.
Among interesting topics I came up with, I stated:
Clearly, there’s something about live coding.
I went through several thought processes. I first had a look at Lua, because lots of people think
it’s cool. However, I don’t like the syntax nor the overall safety the language
gives doesn’t give you.
I remembered that almost a decade ago, when I was 15 or 16 year old, I implemented some kind of a plugins system in C++ for a side project of mine. The idea was simple:
dlopen, dlsym to open a relocatable object / archive (.so / .dll).This is not magic. When you link a crate / library, by default, a lot of compilers will perform static linking. That’s the case of ghc or rustc, in most situations.
If you don’t know yet, a library / crate / dependency statically-linked in a program means that all of its symbols (the ones used in the program at least) are placed in a specific section of the generated executable, so that those symbols have a proper address and “come with the binary”.
Dynamic linking, on the other side, is a way to express a dependency between your binary and some
code, which is most of the time living in a relocatable archive (.so on Linux or macOSX, .dll on
Windows – macOSX also uses .framework but it’s just for the overall idea). When your binary needs
to call a function defined in a dynamically linked library, it’ll open the shared library with the
dlopen function, try to locate the function by giving its (unmangled) name to dlsym or dladdr
for instance, if the function exists, you’ll be able to run its code.
There exists attacks and interesting things you can do with dynamic libraries. Because the code
doesn’t live in the binary, you can for instance replace a legit and safe .dll on Windows with a
malicious one. Or you can patch a buggy dynamic library by shipping a new .dll / .so without
having its dependent to re-compile their applications (which would be needed in case of static
linking). Another funny thing you can do: fraps, a real-time recorder for your video games,
performs some kind of .dll injection by pushing an OpenGL .dll into your binary (Windows allows
this) and replacing some known functions. For instance, the function responsible in swapping your
renderbuffer chains. It can then intercepts pixels regions, adds overlay, etc. Fascinating! ;)
Anyway, the idea is that whenever you link a dynamic library, your compiler / linker will just
insert the required code in your binary (think of dlopen) so that your binary can load code at
runtime. It’s then pretty easy to implement a plugin system:
So you could imagine, as a very simple start example, a loop that would simply invokes such a
function. Whenever you change the library, the code getting ran will automatically changes as well!
So I decided to reproduce that in Rust using the few crates of mine:
spectra: obviously, since it’s the crate that will receive the feature first.warmy: it’s indirect since spectra re-exports it and adds a few things over it, like a
resource system (error messages, timing loading and reloading, etc.).libloading: the Rust way to go with dynamic libraries (it has an unsafe interface though).For unix plateforms, you can see that
libloadingis just a smart wrapper over the functions I mentionned. See for yourself.
Ok, so, let’s try the experiment!
The first thing we must accomplish is very simple: we want to be able to load some (Rust) code at
runtime, inside our application. For achieving that goal, we need to load a dynamic library (.so
on Linux) with libloading::Libray::open.
Once we have the library, we can just look a symbol up with
libloading::Library::get.
In case of a successful lookup, that function returns a value of type Symbol, which implements
Deref for the symbol you’re asking.
Dynamic library typically gathers functions, so we’ll be looking for
fnsymbols.
However, we don’t have any dynamic library yet. We only have… a Rust file – .rs. How can we get
a dynamic library out of it?
This is actually pretty simple. We’re gonna start easily by making a .so that will just contain
a function called hello_world that will just print "Hello, world!" on stdout. You all know how
to implement such a function:
/// hello_world.rs
pub fn hello_world() {
println!("Hello, world!");
}
We make the function
pubso that it gets actually exported when we generate the dynamic library.
Copy that code and put it in, for instance, /tmp.
Then, let’s generate a dynamic library!
cd /tmp
rustc --crate-type dylib hello_world.rs
Once rustc returned, you should see a new file in /tmp: /tmp/libhello_world.so! Here we are!
We have a dynamic library! Let’s try to find our function in it with the handy nm program.
I reckon
nmis already installed on your machine. If not, it should come with packages likebase-devel,build-essentialsor that kind of meta packages.
nm /tmp/libhello_world.so | grep hello_world
0000000000000000 N rust_metadata_hello_world_8787f43e282added376259c1adb08b80
0000000000040ba0 T _ZN11hello_world11hello_world17h49fe1e199729658eE
Urgh. We have a problem. Rust has mangling for its symbol names. It means that it will alter the
symbols so that it can recognize them in a dynamic library. For instance, the mangle version of a
function name foo defined for a type A won’t be the same as the one of foo defined for a type
B. However, in our case, we don’t want mangling, because, well, we won’t be able to lookup the
name up – no one will even try to guess that _ZN11hello_world11hello_world17h49fe1e199729658eE
function name.
rustc has a very simple solution to that: just tell it you don’t want a symbol’s name to be
mangled. It’ll be stored in the dynamic library the way you write it. This is done with the
#[no_mangle] attribute.
/// hello_world.rs
#[no_mangle]
pub fn hello_world() {
println!("Hello, world!");
}
Now recompile with the same rustc line, and run the nm + grep oneliner again.
nm /tmp/libhello_world.so | grep hello_world
0000000000040b70 T hello_world
0000000000000000 N rust_metadata_hello_world_8787f43e282added376259c1adb08b80
Now you can see there exists a symbol called hello_world: this is our symbol!
For our little example here, we’re just gonna load and run the code in a new project.
cd /tmp
cargo new --bin dyn-hello-world
Created binary (application) `dyn-hello-world` project
cd dyn-hello-world
Edit the Cargo.toml file to include libloading = "0.5" in the [dependencies] section. Ok,
we’re good to go. Run a second terminal in which you run this command to automatically check
whether your code is okay:
cd /tmp/dyn-hello-world
cargo watch
Let’s edit our main.rs file.
extern crate libloading;
use libloading::Library;
fn main() {
let lib = Library::new("/tmp/libhello_world.so").unwrap();
let symbol = unsafe { lib.get::<extern fn ()>(b"hello_world").unwrap() };
symbol();
}
It should check. A bit of explanation:
Library::new accepts as first and only argument the path to the dynamic library. In our
case, we just use our freshly generated libhello_world.so.unsafe get call takes the symbol we look for as argument and returns it if it’s found it.Symbol implements Deref, here, we can directly call the function with symbol().Now compile and run the code:
cargo build
Compiling cc v1.0.4
Compiling libloading v0.5.0
Compiling dyn-hello-world v0.1.0 (file:///tmp/dyn-hello-world)
Finished dev [unoptimized + debuginfo] target(s) in 2.63 secs
cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/dyn-hello-world`
Hello, world!
Hurray! We’ve just built our first dynamic library loader!
Ok, now, let’s iterate: we need to compile the Rust code from our Rust code (haha). For doing that,
we’ll need to generate the dynamic library at some place. Because I don’t like to put artifacts in
random places, I like to use the tempdir crate, which gives you a TempDir that creates a new
temporary directory with a random name when you ask for it, and removes it from your filesystem when
the TempDir goes out of scope. We’ll also be using the std::process module to run rustc.
Add the following to your [dependencies] section in your Cargo.toml:
tempdir = "0.3"
Ok, let’s compile a Rust source file into a dynamic library from our Rust source code!
extern crate libloading;
extern crate tempdir;
use libloading::Library;
use std::process::Command;
use std::str::from_utf8_unchecked;
use tempdir::TempDir;
fn main() {
let dir = TempDir::new("").unwrap(); // we’ll drop the .so here
let target_path = dir.path().join("libhello_world.so");
let compilation =
Command::new("rustc")
.arg("--crate-type")
.arg("dylib")
.arg("/tmp/hello_world.rs")
.arg("-o")
.arg(&target_path)
.output()
.unwrap();
if compilation.status.success() {
let lib = Library::new(&target_path).unwrap();
let symbol = unsafe { lib.get::<extern fn ()>(b"hello_world").unwrap() };
symbol();
} else {
let stderr = unsafe { from_utf8_unchecked(compilation.stderr.as_slice()) };
eprintln!("cannot compile {}", stderr);
}
}
Compile, run, and…
cargo run
Compiling dyn-hello-world v0.1.0 (file:///tmp/dyn-hello-world)
Finished dev [unoptimized + debuginfo] target(s) in 0.58 secs
Running `target/debug/dyn-hello-world`
Hello, world!
This piece of code is the first premise of our plugin system. You can see interesting properties:
dyn-hello-world to change its behavior, since it comes from a
dynamic library.However, there’s a problem. Try adding an extern crate to hello_world.rs, like, for instance:
extern crate spectra;
#[no_mangle]
pub fn hello_world() {
println!("Hello, world!");
}
Run dyn-hello-world.
cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/dyn-hello-world`
cannot compile error[E0463]: can't find crate for `spectra`
--> /tmp/hello_world.rs:1:1
|
1 | extern crate spectra;
| ^^^^^^^^^^^^^^^^^^^^^ can't find crate
error: aborting due to previous error
Damn, how are we gonna solve this?
The key is to understand how cargo deals with the dependencies you declare in your Cargo.toml’s
[dependencies] section. To do this, clean your project, and recompile in verbose mode – we’re
like… reverse engineering cargo build!
cargo clean -p dyn-hello-world
cargo build --verbose
Fresh libc v0.2.36
Fresh winapi-build v0.1.1
Fresh winapi v0.2.8
Fresh cc v1.0.4
Fresh rand v0.4.2
Fresh kernel32-sys v0.2.2
Fresh libloading v0.5.0
Fresh remove_dir_all v0.3.0
Fresh tempdir v0.3.6
Compiling dyn-hello-world v0.1.0 (file:///tmp/dyn-hello-world)
Running `rustc --crate-name dyn_hello_world src/main.rs --crate-type bin --emit=dep-info,link -C debuginfo=2 -C metadata=e8b1e5e96f709abe -C extra-filename=-e8b1e5e96f709abe --out-dir /tmp/dyn-hello-world/target/debug/deps -C incremental=/tmp/dyn-hello-world/target/debug/incremental -L dependency=/tmp/dyn-hello-world/target/debug/deps --extern tempdir=/tmp/dyn-hello-world/target/debug/deps/libtempdir-9929bcad6dc8cc47.rlib --extern libloading=/tmp/dyn-hello-world/target/debug/deps/liblibloading-a2854ce154eb4d6f.rlib -L native=/tmp/dyn-hello-world/target/debug/build/libloading-4553b9f132aa813a/out`
Finished dev [unoptimized + debuginfo] target(s) in 0.50 secs
We can see a few things going on here. First, there are some Fresh lines we’re not interested
about. Then cargo tries to compile our application. You can see an invokation to rustc with a
long list of arguments. Among them, two interest us:
-L dependency=/tmp/dyn-hello-world/target/debug/deps.extern crate statements.
--extern tempdir=/tmp/dyn-hello-world/target/debug/deps/libtempdir-9929bcad6dc8cc47.rlib--extern libloading=/tmp/dyn-hello-world/target/debug/deps/liblibloading-a2854ce154eb4d6f.rlibThere’s also a
-L native=…one. This is explained in the manual ofrustcand corresponds to native library we must link against, like a C library, for instance.
As you can see, the path used in -L dependencie=… is pretty constant. It seems it has the form:
/path/to/project/target/<build type>/deps
However, remember that the initial intent was to write a plugin system for spectra, which is a
library. We don’t know the path to the project that will be using spectra, so we must find it in
a way.
Two options:
macro_rules) to automatically insert it at the calling site. However,
I’m not even sure it would work (you need to find a way to have access to cargo’s internal…
maybe via the env! macro?).I chose the second option because it was pretty easy and straight-forward to implement. However, I’m not a huge fan of it – it’s pretty… non-elegant to me. Here’s the code that gives me the root path of the current project a function is defined in:
/// Try to find the project root path so that we can pick dependencies.
fn find_project_root() -> Result<PathBuf, PluginError> {
let result =
Command::new("cargo")
.arg("locate-project")
.output()
.unwrap();
if !result.status.success() {
Err(PluginError::CannotFindMetaData("cannot locate root project".to_owned()))
} else {
let json = unsafe { from_utf8_unchecked(result.stdout.as_slice()) };
let root =
json.split(':').nth(1).and_then(|x| {
if x.len() >= 3 {
Path::new(&x[1..x.len()-3]).parent().map(|x| x.to_owned())
} else {
None
}
});
root.ok_or_else(|| PluginError::CannotFindMetaData("cannot extract root project path from metadata".to_owned()))
}
}
As you can see, I use the cargo locate-project feature, that gives you the root path of the
project the cargo invokation is in. It supports calling it from a subdirectory, which is neat (a
bit like git, actually). Most of the code is removing the JSON sugar over it. It’s pretty unsafe
because if the output format of cargo locate-project changes, this code will basically break.
Pro reminder for myself: write unit tests for that piece of code. :D
I won’t show the PluginError type, it’s not important and it’s spectra current code for the
feature.
Ok, we lack two things:
spectra.Finding the build type is pretty easy: you can use the debug_assertions cfg! argument. It’s set
to true on debug target and false on release. You can actually witness it works with the
following oneliner:
println!("build target is debug: {}", cfg!(debug_assertions));
Ok, now, how do we find our spectra crate’s path? You saw it has a metadata glued to its name in
the previous verbose output.
I didn’t find any satisfying solution. I just came up with a solution. I warn you: it’s not elegant, it’s highly unsafe, but for now, it works. I’ll try to find a better way later.
Here’s the code.
/// Find the spectra crate in the given directory.
fn find_spectra_crate(path: &Path) -> Result<PathBuf, PluginError> {
// we open the directory we pass in as argument to the function
if let Ok(dir) = path.read_dir() {
// we iterate over all of its contained files to find if it has our spectra crate
for entry in dir {
let path = entry.unwrap().path();
match path.file_name() {
// we try to pattern match its name; this is a bit unsafe because it won’t support two
// versions of libspectra… meh.
Some(filename) if filename.to_str().unwrap().starts_with("libspectra") => {
return Ok(path.to_owned());
}
_ => ()
}
}
Err(PluginError::CannotFindMetaData("cannot find the spectra crate; try to build your project’s dependencies".to_owned()))
} else {
Err(PluginError::CannotFindMetaData("cannot find read dependencies".to_owned()))
}
}
If you put those three functions altogether, you can now implement the rustc call without
hardcoding any paths, since they all will be found at runtime and injected in the call!
A bit of hindsight. I didn’t explain that, but it’s a bit obvious: you won’t be able to use all the crates you want in your plugins, for a very simple reason: you must have them installed somewhere. In order for the
find_spectra_crateto find anything, you must havespectra = "…"in the[dependencies]section of yourCargo.toml. If you want to use any crate, you need to add them in the[dependencies]and write a smarter function that parses theCargo.toml’s[dependencies]section and add them to therustcinvokation… which is basically like re-writting a feature ofcargoitself!
I didn’t speak about that, but in spectra, it’s very easy to have a resource to auto-reload if
it changes on the disk. This is done via the warmy crate. You just implement the
Load trait and ask for your type at a
Store by providing a typed key. I won’t
talk too much about warmy – if you’re interested, go read the online documentation
here!
The idea is that I just created a type that wraps both libloading::Library and tempdir::TempDir.
Something like this:
pub struct Plugin {
lib: Library,
dir: TempDir
}
I still don’t know whether I need to implement Drop or not – if the TempDir dies first, I don’t
know whether the Library object is in an unstable state. If not, that means that the whole library
was loaded in RAM at the end of the Library::new call, and that I don’t even need to keep the
temporary directory around!
The interface of a Plugin is not yet defined. I’m writing this blog entry on Sunday night / Monday
morning, while I had that plugin experiment over the weekend. A few thoughts, though:
unsafe fn, calling the safe function or a
trait’s method or whatever.I have a lot of things to say, especially lately. I’ll be posting more thoughts and experiments of mine soon! Thanks for having read through, and as always, keep the vibes!
]]>I came to the realization that I could write a blog entry to discuss designs decisions and, at some extent, what a good design entails. Keep in mind it’s only personal thoughts and that I won’t talk for someone else.
I love mathematics because they’re elegant. Elegancy implies several traits, among simplicity, flexibility and transparency. They solve problems with very nice abstractions. In mathematics, we have a concept that is – astonishingly – not very spread and barely known outside of math geeks circles: free objects.
The concept of free is a bit overwhelming at first, because people are used to put labels and examples on everything. For instance, if I say that an object is free, you might already have associated some kind of lock to that object, so that you can get why it’s free. But we’re mistaken. We don’t need locks to define what free implies. In mathematic, a free object is an object that can’t be defined in terms of others. It’s a bit like a core object. It’s free because it can be there, no matter what other objects are around. It has no dependency, it doesn’t require no other interaction. You can also say that such an object is free of extra features that wouldn’t be linked to its nature.
This free property is a very interesting property in mathematics, because it’s surprisingly simple! We can leverage that mathematic abstraction to software design. I like keeping my softwares as much free as possible. That is – with a more human language to say it – constraining them to keep low responsibilities about what they’re doing.
The important thing to keep in mind is that you should, at first, define what the responsibility domain is all about. Let’s say you’d like to create a library to implement audio effects, like the Doppler effect – that effect actually exists for any kind of wave, but it’s interesting to synthetize it for a sound-related application. If you end up writing functions or routines to play sound or to load audio samples, you’re already doing it wrong! You’d have violated your reponsibility domain, which is, “audio effects”. Unfortunately, a lot of libraries do that. Adding extra stuff – and sometimes, worse; relying on them!
A lot of people tend to disagree with that – or they just ignore / don’t know. There’re plenty of examples of libraries and softwares that can do everything and nothing. For instance, take Qt – pronounce cute or cutie. At first, Qt is a library and an API to build up GUIs – Graphical User Interfaces – and handle windows, events and so on. Let’s have a look at the documentation of modules, here.
You can see how the responsibility domain is huge! GUI, radio, audio, video, camera, network, database, printing, concurrency and multithreading… Qt isn’t a library anymore; it’s a whole new language!
People tend to like that. “Yeah, I just have to use Qt, and I can do everything!”. Well, that’s a point. But you can also think it another way. Qt is a very massive “library” you’ll spend hours reading the documentation and will use a lot of different classes / functions from different aspects. That doesn’t compose at all. What happens when you want to – or when you don’t have the choice? – use something else? For instance, if you want to use a smaller–but–dedicated threading library? What happens if you want to use a database service you wrote or that you know it’s great? Do you wipeout your Qt use? Do you… try to make both work in harmony? If so, do you have to write a lot of boilerplate code? Do you forget about those technologies and fallback on Qt? Do the concepts map to each others?
The problem with massive libraries is the tight bound it creates between the libraries and the developers. It’s very hard with such libraries to say that you can use it whenever you want because you perfectly know them. You could even just need a few things from it; like, the SQL part. You’ll then have to install a lot of code you’ll perhaps use 10% of.
I love how the free objects from mathematics can be leveraged to build simpler libraries here. The good part about free objects is the fact that they don’t have any extra features embedded. That’s very cool, because thanks to that, you can reason in terms of such objects as-is. For instance, OpenAL is a very free audio library. Its responsibility domain is to be able to play sound and apply simple effects on them – raw and primary effects. You won’t find anything to load music from files nor samples. And that’s very nice, because the API is small, simple and straight-forward.
Those adjectives are the base of the KISS principle. The ideas behind KISS are simple: keep it simple and stupid. Keep it simple, because the simpler the better. A too complex architecture is bloated and ends up unmaintainable. Simplicity implies elegancy and then, flexibility and composability.
That’s why I think a good architecture is a small – in terms of responsibility – and simple one. If you need complexity, that’s because your responsibility domain is already a bit more complex than the common ones. And even though the design is complex for someone outside of the domain, for the domain itself, it should stay simple and as most straight-forward as possible.
I think a good API design is to pick a domain, and stick to it. Whatever extra features you won’t provide, you’ll be able to create other libraries to add those features. Because those features will also be free, they will be useful in other projects that you don’t even have any idea they exist! That’s a very cool aspect of free objects!
There’s also a case in which you have to make sacrifices – and crucial choices. For instance, event-driven programming can be implemented via several techniques. A popular one in the functional programming world nowadays is FRP. Such a library is an architectural codebase. If you end up adding FRP-related code lines in your networking-oriented library, you might be doing it wrong. Because, eh, what if I just want to use imperative event-driven idioms, like observers? You shouldn’t integrate such architectural design choices in specific libraries. Keep them free, so that everyone can quickly learn them, and enjoy them!
I like to see good-designed libraries as a set of very powerful, tiny tools I can compose and move around freely. If a tool gets broken or if it has wrong performances, I can switch to a new one or write my very own. Achieving such a flexibility without following the KISS principle is harder or may be impossible to reach.
So, in my opinion, we should keep things simple and stupid. They’re simpler to reason about, they compose and scale greatly and they of course are easier to maintain. Compose them with architectural or whatever designs in the actual final executable project. Don’t make premature important choices!
]]>As an experienced Software Engineer, I know that I need to multi-task and work on many different projects in parallel, with different chronicity. Some new tasks can be done only a couple of minutes after discovering / creating them, and some others will span on days, weeks if not months. That leads to having many parallel things on-going, and since I want to do my job as correctly as possible… I take notes. I take notes about pretty much everything, and I’ve been using note-taking apps more and more.
Years ago, as I discovered Doom Emacs, I also discovered org-mode. I was blown away by the features brought by
Doom Emacs, and a bit puzzled with other things. For instance, the agenda (I rarely care about ETA for my personal
notes; that’s something that is tracked at the company-wide level, for instance in the infamous Jira or whatever you’re
using for project management in your team — and I just don’t care on my spare-time). Also, I was a bit doubtful about
having notes laid as plain text (yes, org-mode formatted, but still text). What it means is that, there is no database
nor state. Everytime you want to get a list of your TODO, org-mode has to go through all your .org files and
generates the list for you. Back then, I found that a bit weird, but today, I have changed my mind.
The way I take notes, whatever I do, is to minimize the note files I create. For instance, at work, I have many notes
about commands to run with a specific product. Instead of having a note That product, I tend to create smaller notes,
more scoped, like That product - deploy, That product - how to configure, etc. That allows two important things:
Yes, fuzzy searching. Whatever the tool I use, I often need to search for notes, and either I search by note names, or by content.
A couple of months ago, I wrote mind, a rewrite of mind.nvim. I designed that system to be a tree editor, in which nodes are either “document node” containing some metadata and a path to a file, or a “link node”, with metadata and URL. Those tools were very useful to me, and I’ve been using them for a long time.
However, as I moved more and more into the UNIX world with kakoune and kak-tree-sitter, I realized that I should
maybe revisit the way I think about notes. For instance, what I often need is to list the on-going tasks I’m doing,
and updating them. Oftentimes (if not all the time), tasks emerged from a context, like a discussion with a colleague,
some notes / brainstorming with myself I’m doing / a meeting / etc. And mind, Notion — or whatever that is using
an external state — are not very suited. The problem relies in interruption. You’re in the editor of your choice,
and then you have to switch to another window, look for the task / notes, use some keybindings to change the task to
switch it from TODO to WIP or WIP to DONE or WONTDO or whatever.
As I was working more and more with mind, I realized this kind of workflow was starting to annoy me.
You start to see the pattern with my blog posts: I enjoy the UNIX world a lot. And even though mind is perfectly
fine regarding that (I was even able to capture notes by using tmux’’s display-popup with mind), I wanted to
switch to something… easier, yet still UNIX.
Then I started to think again about org-mode. See, org-mode is eventually just a format specification. The Emacs
mode is the reference implementation, with major modes, keybindings etc. but if Emacs can edit and do stuff to .org
files, then the concept is transposable to other tools. The question is: what is the core of org-mode? The idea
behind org-mode is that there is no state: the notes themselves are the state. There is no need for an external tool.
Any tool can open an .org file and edit it. Yes, convenience is better to navigate .org notes, but that’s just a
matter of tooling. If you want to open and edit an .org file using your favorite editor, you should be able to.
Then, for the rest, org-mode has a couple of interesting and useful concepts:
.org files.* TODO can be used
at the beginning of a line to start a TODO item, that will appear in the listing. That looks arrogantly innocent, but
it’s really powerful, and I’ll explain why just below.* TODO talk to Bob about service-epsilon. Later on, I might create a note Epsilon and move the TODO item into
the note. Since there is no state, the TODO will still appears in my todo-list.Back from holiday Oct 2023, which contains many TODO items taken from the
backlog I read on Slack (basically, stuff my colleagues did and that I want to catch up with). At some point, I will
be done with all of that, so instead of keeping DONE notes around (that will show up in my DONE listing), I can
just archive that note. It will not appear anymore in my fuzzy search results.All of that is really exciting, because it makes note taking easy and seamless; it doesn’t require any state, and you can pick the tools you want to implement the workflow.
Yes, that’s what I think is the most important thing here. I do not want to use org-mode for now. I like Markdown.
Even though I agree .org files are more powerful, Markdown, at least for now, is more than enough to me. However, I
wanted to augment it to do things slightly similar to org-mode.
And so I did it. I created in my kakoune a couple of commands to:
~/notes/capture.md.~/notes/notes/.~/notes/archives/.~/notes/journal/<year>/<month>/<day>.md. It’s super easy with kakoune since you have
%sh{} expansion. Just use the date command you’re done!TODO, WIP, DONE or WONTDO items.buffer keybindings to jump to the place where the
item is located. For instance, if I list TODO items, pressing <ret> on a line will open the appropriate file and
move my cursor to the right line and column where the TODO item is defined.:learn: or :hard: or whatever.buffer) keybindings to switch tasks to different status.<!-- github_project: <user>/<project> -->,
usually at the end of the file, allows me to press a keybinding on issue numbers (like #123) to automatically open
them in my browser.git commands to rebase / push.I plan on releasing the
.kakfile containing all of that if people are interested.
The great thing is that most of the features, like fuzzy searching, are implemented via external tools. I use fd and
rg fuzzy search todo items. There is nothing to do in kakoune besides gluing everything together.
This setup has been my daily driver for months now and I really enjoy it. Being able to take notes and just slide in a
- TODO I need to check that right in the middle of the file and having it appear in a listing later is a really
powerful aspect of this workflow… and org-mode was doing that all along ever since. Eh, it almost makes me nostalgic
of my time using the Emacs plugin!
I hope that gave me some (not so fresh) hindsight about note taking and task management (and journaling!). Have fun hacking around!
]]>Just for the record, the initial goal I had in mind was to parse a subset of GLSL for my spectra demoscene framework. I’ve been planning to write my own GLSL-based shading language (with modules, composability, etc. etc.) and hence I need to be able to parse GLSL sources. Because I wanted to share my effort, I decided to create a dedicated project and here we are with a GLSL crate.
Currently, you can successfully parse a GLSL450-formatted source (or part of it, I expose all the intermediary parsers as well as it’s required by my needs to create another shading language over GLSL). See for instance this shading snippet parsed to an AST.
However, because the project is still very young, there a lot of features that are missing:
Error(SomeVariant) flag, which is completely unusable;If you’re curious, you can start using the crate with glsl = "0.2.1" in your Cargo.toml. You probably want
to use the translation_unit parser, which is the most external parser (it parses an actual shader). If you’re
looking for something similar to my need and want to parse subparts of GLSL, feel free to dig in the
documentation, and especially the parsers module, that exports all the parsers
available to parse GLSL’s parts.
Either way, you have to pass a bytes slice. If your source’s type is String or &str, you can use the
str::as_bytes() function to feed the input of the parsers.
Not all parsers are exported, only the most important ones. You will not find an octal parser, for instance, while it’s defined and used in the crate internally.
If you think it’s worth it, I’m looking for people who would like to:
AST -> Result<SPIRV, _>, for being pragmatic;Happy coding, and keep the vibe!
]]>foo that takes a function F as sole argument and
returns its result.F is a unary function that takes a function Λ as sole argument.Λ doesn’t take any argument and return a type T.In Haskell, you would represent this scenario like this:
data T = T -- we’re not interested in this type yet
-- v this is F
--------------
foo :: ((() -> T) -> b) -> b
-------
-- ^ this is Λ
-- or for people less familiar with Haskell, here’s a decomposed version (but still the same thing)
type Λ = () -> T
type F b :: Λ -> b
foo :: F b -> b
Note: it’s pretty much known in the Haskell world but maybe not in others: if you have a value, like
a, it’s isomorphic to a unary function that returns this value and takes()as single argument –() -> a.
Note²: for curious, in Haskell, the syntax
a ->. bis a linear arrow. More about this on linear Haskell. I didn’t include the syntax in the snippet because it would make the post too complicated for our purpose.
Note³: this is a rank-2 type function, because it’s a function that takes a function (1) that takes another function (2). There’re levels of “nesting” there.
In Rust, argh… Rust is not as powerful as Haskell for complex abstractions – even though we’re working on making it better and better. To me, Rust competes with Haskell, but the current blog post will show you that it still has a lot to learn from its parent! :)
Let’s sketch something in Rust.
struct T; // we’re not interested in this type yet
fn foo<F, B>(f: F) -> B where F: FnOnce(???) -> B {
f(|| 42)
}
What should we put in place of the ??? placeholder? Clearly, looking at the Haskell snippet,
we want a function that produces a T. Something like that:
fn lambda() -> T
So we could use the type fn () -> T, but that will not work, because a lambda has not such a
type – you can see the lambda || 42. Let’s try something else.
fn foo<F, B>(f: F) -> B where F: FnOnce(impl FnOnce() -> T) -> B
If you look at the impl Trait RFC, you see that it’s a Rust construct to build a contract that lets you handle a value through traits without knowing the type – we call that an existential. The type is picked by the callee, which in our case is the function itself.
However, this syntax doesn’t work because – as for now – impl Trait works only in return position
and the consensus seems to have agreed that when impl Trait will be implemented in argument
position, it will not mean an existential, but a universal, e.g.:
fn bar<I: Iterator<Item = u32>>(i: I)
fn bar<I>(i: I) where I: Iterator<Item = u32>
fn bar(i: impl Iterator<Item = u32>)
Are all the same thing.
Note: I’ve made a ranting about the decision to give universal semantics to
impl Traitin argument position here. I don’t want to start a type theory war, if it’s the consensus, then we have to accept it and move on. It’s just a bit a pity. Topic closed, let’s get back to our sheep!
So we cannot use that. Argh! Let’s try something else then!
fn foo<F, G, B>(f: F) -> B where F: FnOnce(G) -> B, G: FnOnce() -> T
This cannot work because here you have two universals F and G, meaning that G will be picked
by the caller while you want to decide of its type directly in your function (remember the lambda).
fn foo<F, B>(f: F) -> B where F: for<G: FnOnce() -> T> FnOnce(G) -> B
I like this one a lot. It uses a concept called HRTB in Rust which you can see by the
for<_> syntax. However, this will not work because as for today, HRTB only works with lifetimes.
Ok, that seems a bit desperate!
Hm, how about this:
fn foo<F, B>(f: F) -> B where F: FnOnce(Box<FnOnce() -> T) -> B
Even though that seems to typecheck, you won’t ever be able to use Λ here because you cannot move
out of a Box with FnOnce. This limitation is removed with the FnBox type:
fn foo<F, B>(f: F) -> B where F: FnOnce(FnBox() -> T) -> B
However:
FnBox is unstable and will only work on nightly right now.Ok, that seems desperate. Period.
Well, if we only stick to those tools (i.e. functions, type variables and trait bounds), currently, yeah, there’s no solution. However, if we add the traits to our toys, we can work around the problem:
trait GConsumer {
type Output;
fn call_once<G>(self, f: G) -> Self::Output where G: FnOnce() -> i32;
}
fn foo<F>(f: F) -> F::Output where F: GConsumer {
f.call_once(|| 32) // hurray!
}
This snippet is actually quite interesting. We introduce the existential via a trait. The
existential is encoded with the G type variable of the call_once method of GConsumer. If you
look at that function from foo perspective, G is definitely an existential! – and it’s a
universal for call_once.
I really like this idea, because it’s simple and legacy Rust. But I feel a bit confused, because
it’s simple to define but hard to use. Instead of just passing a closure to foo, people will now
have to implement a separate type on which will be called call_once. That’s a lot of boilerplate.
I hope we’ll come to a better solution to this problem – if you can do it with any trait, it should
be possible with FnOnce and a few candies into the Rust language itself.
That’s all for me for today. Keep the vibes and have a nice weekend!
]]>Cheddar is a GLSL superset language. What it means is that most of the GLSL constructs and syntax you’re used to is valid in Cheddar – not all of it; most of it. Cheddar adds a set of features that I think are lacking to GLSL. Among them:
However, Cheddar is not:
The documentation is still a big work in progress but most of it should give you enough information to get started.
Cheddar was imagined and designed while I was working on spectra, a work in progress demoscene crate I’ve been working for a while now. I released two demos thanks to spectra – this one and this one. Because those demos are very meh to me, I decided to enhance my tools. Among them was the need to write shaders a better and easier way. Then Cheddar got born. I used Cheddar while preparing other demos, and I was talking about it on IRC and Reddit, people seemed to be interested – I even had a friend writing demos in C++ interested!
So here it is. Please, provide feedback if you try it, should you like it or not!
Disclaimer: the current state of the language is pretty experimental and unstable. There’s no semantics checking, for instance.
You can read the full tutorial and design document on the official documentation page.
Thanks for having read me, and as always, keep the vibe!
]]>You can read the first article here.
I feel like I’m either doing something wrong… or that something is wrong with using pest to build typed ASTs. I spent a whole day writing the GLSL450 PEG. After having the compiler stop failing with left-recursion errors, I was finally able to test my crate!… or was I?
First thing first, I decided to write tests to see whether I can recognize identifiers. I’m a TDD enthusiast – don’t get the idea twisted: writing tests is boring; but writing tests will save your life and prevent small and cute kittens joining a project from drowning (true story).
I have a PEG rule for that, identifier. So the idea is the following:
// we set the error type to String for that first version
fn parse_identifier<'a, S>(input: S) -> Result<syntax::Identifier, String>
where S: Into<&'a str> {
// here we get a set of pairs that match the identifier
let pairs = Parser::parse(Rule::identifier, input.into()).map_err(|e| format!("{}", e))?;
// since only a full string identifier would match, we can just simply ask to retrieve the string
// that composes the pair
Ok(pairs.as_str().to_owned())
}
As you can see, we have lost the grammar information that states that the identifier rule outputs
a single string – see in the code above how we get a Pairs object, assigned to pairs. This is
my first concern: the Rule type should contain information about what kind of values it was
matched against.
A more drastic example is the primary_expression rule, that defines expressions that can be either
an identifier, a float_constant, int_constant, bool_constant or any general expression with
parenthesis around. Its parser code would look like this:
fn parse_primary_expression<'a, S>(input: S) -> Result<syntax::Expr, String>
where S: Into<&'a str> {
let pairs = Parser::parse(Rule::primary_expression, input.into()).map_err(|e| format!("{}", e))?;
// we know we only have one expression, so this statement seems a bit out of nowhere here
let pair = pairs.next().unwrap();
// now, we know the primary expression is correctly tokenized, so we’re interested in “what’s
// inside”; here, we HAVE to look at the grammar again to check what are the possible variants;
// only one possible, let’s take the next pair then as well…
let inner = pair.into_inner().next().unwrap();
// here the pair represents the “real” and “useful” sub-rule that was matched; in order to write
// that match block, we also have to look at the grammar to see all the possible variants
match inner.as_rule() {
Rule::identifier => {
// the pair represents an identifier… we can just turn it to string and return it
Ok(syntax::Expr::Variable(inner.as_str().to_owned())) // (1.)
}
Rule::float_constant => {
// the pair represents a float constant; let’s just parse it
let f = inner.as_str().parse().unwrap();
Ok(syntax::Expr::FloatConstant(f))
}
Rule::etcetc. => {
// etc. etc.
}
_ => unreachable!() // eeew (2.)
}
}
As you can see, we have several problems here
Pair so that we can compose them… that strangely
resembles how the nom parser is actually written… :)Result::unwrap, a Parse::parse failure, etc.Maybe I’m plain wrong and that there’s an easier way to do that, but I’ve been writing the parsers of only a few rules (on more than fifty) and I already cringe.
All of this brings a brand new problem: since we’re smart developers and want to write the most
reusable code, we want to be able to write the parser of primary_expression once and reuse it in
other parsers that might need it – like postfix_expression, expression, etc. The current code
that consumes Into<&str> doesn’t allow this as pest parses to Pairs. So let’s just write our
functions to take Pair as inputs!
But… now we don’t have a proper public facing interface for our crate. Surely, I don’t want people to even see that pest is used – they’ll just see it as a dependency, but I don’t want any pest symbols in my public interface.
That problem can be solved by introducing a trait, Parse, for instance, that has the right
parsing signature:
/// Class of types that can be parsed.
pub trait Parse: Sized {
/// Parse an item from a string.
///
/// Store errors as strings for convenience for now.
fn parse<'a, S>(input: S) -> Result<Self, String> where S: Into<&'a str>;
}
Then, to support a new syntax symbol, we must:
Pair parser.Parse trait for this type, using the Pair parser from just above.Pair parser in the implementations of other Pair parsers if
needed.I must say: I’m astonished by the complexity and verbosity – and dangerosity! – of all of this. This is “on me” because I’ve constrained myself here:
parse any type from the grammar / syntax. This is currently the
case with the nom parser, that uses a similar trait. To implement the trait, I just defined a
very small macro_rules that simply invokes the required parser for the given type.
Code here.My main problem here is that there’s no way to have pest behave as a scannerless parser: you will always have this two-phases lexer-parser:
I would be okay with this, however:
Once compiled, a pest grammar is basically a very big and flat Rule type. If you have this PEG:
WHITESPACE = _{ " " | NEWLINE }
number = { ASCII_DIGIT+ }
op = { "-" | "+" | "*" }
expr = {
number ~ (op ~ expr)? |
"(" ~ expr ~ ")"
}
A possible AST – one I would definitely write – for that would be:
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Op {
Minus,
Plus,
Mul
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum Expr {
Number(i32),
Op(Number, Op, Box<Expr>)
}
However, pest outputs something like this:
enum Rule {
WHITESPACE, // very useful, thank you!
number,
op,
expr
}
And implements the Parser trait to yield Pairs according to the variant of Rule you choose to
parse with. Instead, a more practical encoding of rules would be:
struct Rule_number;
impl SuperMegaParser<i32> for Rule_number {}
struct Rule_op;
impl SuperMegaParser<Op> for Rule_op {}
struct Rule_expr;
impl SuperMegaParser<Expr> for Rule_expr {}
That is, Parser::parse(Rule_expr) wouldn’t yield Pairs anymore, but Expr. In order for this
to be possible, we would need to tell our scannerless parser what to do when matching a rule:
WHITESPACE = _{ " " | NEWLINE } // who cares about whitespaces, seriously? they’re MUTED! :D
number: i32 = { ASCII_DIGIT+ => |s| s.parse().unwrap() }
op: Op = {
"-" => Op::Minus |
"+" => Op::Plus |
"*" => Op::Mul
}
expr: Expr = {
number ~ (op ~ expr)? => |(n, opt_op_expr)|{
match opt_op_expr {
Some((op, expr)) => Expr::Op(n, op, Box::new(expr)),
None => Expr::Number(Number(n))
}
}|
"(" ~ expr ~ ")"
}
This would be perfect to me. And I reckon it’s pretty much what lalrpop uses.
I’ve been feeling on and off about pest lately, to be very honest. At first I was amazed at the
PEG file, because, yeah, PEG is lovely to work with. However, I think GLSL450 is a really good
candidate to test a lexer / parser and to that matter, the current vanilla pest is a nah to me.
It makes the code terribly bloated and harder to maintain – while it should be easier! – than the
reference nom implementation. The very reason to that is that even if I had to write thousands
line of macros calls – yiiiik – with nom, those macros are correctly typed. The identifier nom
parser is a function taking bytes and outputing… syntax::Identifier. Same thing for all other
parsers.
I had to try pest. From my – limited – experience of it, I’d say it’s definitely not a
parser. nom is – a scannerless parser. pest is a lexer that does a few parsing work
to sort and fold the lexemes (tokens) into a tree. You can see pest’s output as a big regular
acyclic tree holding pairs of pointers (representing tokens in the input source). That’s everything
pest gives you. In order to turn that representation into a typed AST, a lot of work is awaiting
you. I’ll take a few hours / days to think about what I should do and work on other projects in the
meantime. I doubt I’ll keep going with pest because I feel like I’m going to spend entire days
repeating myself by looking the grammar up to, frustrated, shout on IRC that I’m writing untyped
code while I have static assurances (i.e. remember: the grammar is read at compile-time) that I will
never have to look at the Rule::external_declaration while parsing a Rule::postfix_expression.
And as I might be changing the grammar rule a bit when refactoring / fixing bugs, I really really
don’t plan to pay a visit to destination fucked yet.
Someone on reddit suggested me to have a look at pest-ast. Even if this looks promising, it doesn’t seem finished and I think it should be merged into pest directly when done.
That’s all for me today. I might try a bit further to see if I find a way to make my pest experience less painful but if I don’t, I might try that lalrpop cutie in a few hours / days! As a final note, I don’t want to state that pest is bad, I think it’s pretty cool and that it does its job greatly, but my need (typed AST) might be off its scope. That’s all to note, I’d say. :)
Keep the vibes.
]]>