Devlog

Feb 12, 2025

Syndicate-friendly keywords

Originally, the Klee interpreter plan was a very traditional Scheme-like language. The main issue with this is that it misses out on all the lovely features of the Syndicated Actor Model that are particularly useful for rendering responsive graphics. The best example of this is the during behavior which is a common theme in syndicate implementations. This behavior frames an actor’s action to when a value is being asserted. Adding this type of behavior to Klee would allow for shape definitions like this:

(define (my-square name)
  (during (record 'square name)
    (stroke #000
      (square 0 0 10px))))

This shape labelled with the symbol my-square takes a name parameter, and only draws the square while a record of name square with the same name value is being asserted to the Klee dataspace.

I also think a keyword like collect based on the folk computer “collecting matches” phrase would be useful for rendering things based on dynamic sets of assertions. For example rendering a dynamic point cloud could look like this:

(define (draw-point p)
  (stroke #000
    (point p)))

(define (my-points)
  (collect ('l '(record 'point p))
    (map draw-point l)))

Also needed, is the ability for shapes to make assertions. This will come in two primary ways:

  • controllers
  • raw assertion keyword

Controllers are shapes that assert, input events like mouse/keyboard to the dataspace when they are interacted with. Quick example would look like:

(define (c-square)
  (on-mouse-down 'c-square
    (stroke #000
      (square 0 0 10))))

When a mouse down event occurs inside the area defined by the square call, the controller will assert a mousedown event with the associated symbol c-square. Any observing actor in the dataspace can then react to this assertion and possibly update the view.

Raw assertions are exactly what it sounds like, the ability to make assertions using preserves format. These can be combined with the previous keywords.

(define (c-square)
  (during <mousepressed>
    (during <shift-pressed>
      (assert (record 'hello))
      (stroke #000
        (square 0 0 10)))))

Here, when both the mouspressed and shift-pressed records are asserted in the dataspace, the c-square procedure asserts a hello record to the space. I’m not exactly sure of great uses for this yet, but it might allow for some interesting extensions to the system. In general, once I have the core system working, I want to spend a lot of time working on making the system growable. A big part of this effort might be giving this system some sort of metaobject protocol, something to make it so I’m not having to hard-code every new object I want to add on the rust side, as it feels very fragile.

Feb 13, 2025

Rethinking Top-level evals

In the notes above I wrote:

“This is an important nuance of the klee system, you cannot for example send the expression (+ 1 2) to the dataspace and expect for the system to assert a response, the only expression which is ever evaluated at the top level is the view expression.”

We have this wonderful substrate of a dataspace that allows us to communicate between processes really well, limiting the control of the entire display surface to a single assertion greatly hampers the collaborative possibilities. For large swarms of actors contributing to a single klee dataspace, I think this might become an issue where actors might have to “fight” for the single view assertion to make large changes to the client.

I think the better option is to allow for arbitrary top-level execution, and execute each top-level function with the environment of the client surface. So for example:

(define (my-rect)
  (fill 'red
    (rect 0 0 100% 100%)))
(my-rect)

Will execute my-rect with the ParentWidth and ParentHeight environment variables set to 100% of the window’s dimensions. Thus this will draw a red rectangle that covers the entire window no matter how it is resized!

I think the view function will still be useful, it’ll still provide the first start point for each frame execution, and it will also be used to set the screen dimensions and other important wayland client properties.

One other thing to consider is that loose top-level evals ruin any concept of proper ordering, so this may create issues with draw ops happening out of intended order. To compensate for this, we can create a surface object that will have a z-index.

(define (my-rect color)
  (fill color
    (rect 0 0 100% 100%)))

(surface 0 0 50% 50% 1
  (my-rect 'blue))

(my-rect 'red)

Let’s say the surface function takes properties x, y, w, h, and z-index in that order, so the surface function call above will draw with a z-index of 1. The default z-index is the “sea-level” of 0. This full set of assertions will end up drawing a full red background that resizes with the window, and then a blue rectangle on top which will take up the top-left quadrant of the window, and responsively resize.

Surfaces can potentially map nicely to wayland subsurfaces under the hood, and we can probably optimize the interpreter’s interaction with the compositor to efficiently manage redraws.

Feb 15, 2025

The Klee Numerical Tower

One important feature of traditional scheme implementations is their use of Numerical Towers, and Klee is no different.

Eventually, I would love to implement the full scheme numerical tower on top of the Klee-specific values, but for now this is the current Rust description of Scalars (e.g. raw values without direction) and 2d vectors:

#[derive(Debug, Clone, PartialEq)]
pub struct Polynomial {
    t: f64, //Placeholder
}

#[derive(Debug, Clone, PartialEq)]
pub enum Scalar {
    Int(i64),
    Float(f64),
    Px(i64),
    Cm(i64),
    ParentWidth(f64),
    ParentHeight(f64),
    ViewWidth(f64),
    ViewHeight(f64),
    RootUnit(f64),
    Unit(f64),
    Polynomial(Polynomial),
}

#[derive(Debug, Clone, PartialEq)]
pub struct Vec2 {
    x: Scalar,
    y: Scalar,
}

The major takeaway is the inclusion of relative units, and also the Polynomial definition. Relative units, as mentioned briefly above, allow for the definition of shapes that adapt to changes in their parent container’s shape. They work by “snapping into” absolute values when executed inside an eval environment that has ParentWidth, ParentHeight, ViewWidth and ViewHeight values. The top-level environment always has these value set based on the current underlying wayland surface dimensions.

Polynomials represent combinations of these various units via +, -, *, / operations. What is exciting about this is that you can use this to set extremely precise responsive dimensions. For example, you could define the height of a rectangle to be a responsive value of (+ 10vh 20px) which is equivalent to 20px more than 10% of the current view height. All of this is very similar to the current calc() function available in modern CSS.

The idea of RootUnit and Unit also pull from CSS, these are equivalent to the rem and em units, but freed from their inherent ties to font-size. A good example use could be for defining the size, in pixels, of a grid unit for a grid layout, and then defining your layout in terms of these RootUnits so that everything sits on this grid.

As a note, I would really love to have cm be the base absolute unit users think in terms of over pixels, especially as higher DPI screens continue to improve. Getting 1cm to equal 1 IRL cm will be a challenge to get working reliably but I believe we can figure out a decent system for calculating this using wayland to gather display info.

Feb 16, 2025

We Have Eval!

Got my first custom object to evaluate properly which is exciting!

Here is the first eval test that passed:

#[test]
fn test_rel_len_with_view_dim() {
    let v = KleeView { w: 200.0, h: 200.0 };
    let mut env = Rc::new(RefCell::new(Env::new_view(v)));
    let obj = Object::List(Rc::new(vec![
        Object::BinaryOp("+".to_string()),
        Object::ParentHeight(1.05),
        Object::ParentWidth(2.00),
    ]));
    let result = eval(&obj, &mut env).unwrap();
    assert_eq!(result, Object::Float(6.10));
}

The ParentHeight and ParentWidth objects correctly get evaluated to be 1.05% and 2% of the containing environments dimensions, which in this case is 200x200. They get eval’d to floats, which then get added together to reach the intended output of 6.10.

Here’s to many more!