Lately, I'm really active on [Mastodon](https://raphus.social/@MaddieM4/116564845356425656), more so than anywhere else by a decent margin. Not everything I post there is very dev-blog-like - I try to post a music video by the band Gunship once a day, I post about weird food and beverage combos I'm trying, [I do a little clowning around](https://raphus.social/@MaddieM4/116567167454221680), I talk a lot more about my stance on AI (which is to say: "no"), even a bit of politics. I'm trying out using [Dillo](https://dillo-browser.github.io/) for more of my browsing lately, and while I doubt I could daily drive it with my current needs, I'm pleased to see how close to correctly my website renders in such a feature-lite browser, and it should be totally possible to tweak my site to look better in that kind of context.

![[dillo.png]]

In addition to this, I post a lot about Prone development, in a more microblogging format. Recently, I made [a thread](https://raphus.social/@MaddieM4/116564502289796702) that was snippety enough to really benefit from sane formatting (Mastodon can render formatted posts from other Fediverse clients, but can only _create_ posts in plain text, which is a baffling and technically arbitrary limitation), so between formatting and length, it really deserved to be a blog post. So I'm going to clean it up for y'all here.

---------------------------------------------

This thread might take awhile. I'm full in medical struggle. But I'm pretty proud of the work on [#pronelang](https://raphus.social/tags/pronelang) that I managed to squeeze in between the disruptions and chaos in my day, and I want to talk about it.

It doesn't look like a ton of progress. It's just one item on my `TODO.md`, really half of one (I did the other half yesterday). But it required a lot of side quests that will have long-term value beyond just unlocking the "eval" ability on `prn_assign_t` objects.

![[screenshot-git-commit.png]]

![[screenshot-todo.png]]

A little context: we need to represent "things you can say in Prone source code" as C objects, and the interpreter is able to process those objects in order to execute the code. I used to have a `Construct` type that handled a big random grab bag of these responsibilities, but that turned out to be a bad decision, so I've been slowly adding dedicated standalone types for each of these jobs, and paring down the `Construct` type. Assignments (`x = 3`) are the last piece of functionality left in these legacy Construct objects. Once I've finished making the `prn_assign_t` type, the old Construct type can just be straight up deleted. There will be rejoicing in the streets! Ding dong, the witch is dead.

In C, types can't actually perform actions, and I've praised this as a good thing so many times. What you can have are, separately, types and functions.

```c
// A type.
typedef struct {
  prn_rc_t rc;
  prn_ltext_t lvalue;
  prn_dv_t rvalue;
} prn_assign_t;

// A function that uses it.
prn_istate_t *prn_eval_assign(prn_istate_t *, prn_assign_t *) {
  // do stuff
}
```

This means you actually have a decision point here in how you organize your code, both in terms of function names and which files they live in.

```c
// Option A: Noun-centric.
// This function is defined in src/istate.c.
prn_istate_t *prn_istate_eval_list(prn_istate_t *state, prn_vvec_t *list);

// Option B: Verb-centric.
// This function is defined in src/eval.c
prn_istate_t *prn_eval_assign(prn_istate_t *state, prn_assign_t *assign);
```

Once upon a time, I wrote a bunch of code in Option A form, and that was a mistake borne of old object-oriented habits, even when I knew better. Now I'm cleaning up my mess.

So you'll notice I'm actually cleaning up two messes at the same time: legacy constructs, and noun-oriented code organization. This means every step of the way, I'm doing more work in more uncharted territory, and creating a schism between noun-oriented and verb-oriented code coexisting for awhile, but I feel okay with that trade. Even if I'm going slowly, I'm making the new stuff *right*, rather than making code I'm going to immediately need to fix (where the fixes are, essentially, rewrites). This isn't a dig on more incremental refactoring - that's saved my bacon before - but deciding when to be a headstrong purist vs a cautious improver-by-inches is an art, one I'm still refining with practice, and in this case, a war on two fronts feels worth it to me.

This is why creating the `prn_assign_t` type has meant creating _so_ many new header/source/test combos. Before, I might create `include/assign.h`, `src/assign.c`, and `tests/assign.c` respectively.

And in that little slice of the codebase, there'd be a mix of reference counting logic, object construction, how to produce a "repr" string, equality testing, how to eval an assignment, how to convert back and forth between "thunk" form, make the parser produce `prn_assign_t` instead of `Construct` when it sees `x = 3`, etc.

Now, when I want to support object construction, I need to create a new `include/mk.h`, `src/mk.c`, and `tests/mk.c`. If those files already existed, I could just make them a little bigger by copying and pasting existing code that's kinda close to what I need. But now, I need to make a bunch of novel judgment calls about the conventions of the `prn_mk_foo` functions.

So that's what I've been dealing with lately regarding evaluation, too.

Let's talk about what evaluation is, because one of the things that can get you a little lost is that evaluation is different from execution. Execution is when you call something with parentheses, like `plus(1, 2)` is execution. Objects are executable if you can use the parentheses operator on them, so to speak - in that example, the `plus` function is an object that can be executed.

Evaluation is a little zoomed out, and talking about it is like talking about water to a fish. Simple objects always evaluate to themselves. Evaluate a string like `"foo"`, you'll get `"foo"` back out. But some objects, when you evaluate them, do some processing, some simplification work. A function _call_ for example. `plus` is executable, but `plus(1, 2)` is evaluable, and when you evaluate it, it causes `plus` to be executed with args 1 and 2. In fact, when you call a function, each argument is evaluated first before passing it in, which is why `plus(1, plus(2, 3))` turns into `plus(1, 5)` before running.

So we're sprawling over a lot of topics here, but yesterday's work involved making `prn_eval_assign(...)` work, tests and all. Now here's the problem: we need to hook it into the bigger system of "here's a dynamic value, it could be any type. Look at the tag and decide how to evaluate it." We already have infrastructure to do this, it's just in the old world, the noun world. It looks like this.

```c
IState *istate_eval(IState *state, DV input) {
  switch (dv_typecode(input)) {
  case DV_TC_IDENT:
    return istate_scope_get(state, dv_to_ltext(input));
  case DV_TC_LIST:
    return istate_eval_list(state, dv_to_list(input));
  case DV_TC_CONSTRUCT:
    return istate_eval_construct(state, dv_to_construct(input));
  case DV_TC_CALL:
    return prn_call_eval(state, dv_to_call(input));
  default:
    return istate_set_value(state, input); // pass through unchanged
  }
}
```

Ignore the `IState` vs `prn_istate_t` discrepancy, they're the same type with aliases. Different mess to clean up later.

Looking at that last snippet, you can see, we handle a couple different special objects that actually evaluate in interesting ways, and then have a catch-all for simple objects at the end. And this, somehow, someway, needs to call `prn_eval_assign`, our fancy new function with a verb-forward naming convention.

There's a couple different approaches I could use to incrementally migrate from the noun style to the verb style, but what I eventually landed on was creating a `prn_eval_dv` function that looks like this:

```c
prn_istate_t *prn_eval_dv(prn_istate_t *s, prn_dv_t dv) {
  switch (prn_dv_typecode(dv)) {
  case PRN_DV_TC_ASSIGN:
    return prn_eval_assign(s, prn_conv(dv, dv, assign));
  default:
    return prn_istate_set_value(s, dv);
  }
}
```

It doesn't handle everything yet, but then I can turn the `prn_istate_eval` fallback logic into `return prn_eval_dv(state, input)`, and then incrementally move logic from the old to the new, and eventually kill off `prn_istate_eval`, changing the call sites to point to the (now-complete) `prn_eval_dv` instead.

So, writing `prn_eval_dv` is actually really easy, if you have the mental context. It's almost turn-your-brain-off shit. The hard part is TESTING it.

What I figured out is that you can have the tests for `prn_eval_assign`, `prn_eval_list` etc. be really detailed and comprehensive. All you need for testing `prn_eval_dv` is making sure it dispatches to the right specific function, because that's all `prn_eval_dv` is: dispatch. Which means, what you really want to test is that, if you do these two things:

```c
s = prn_eval_dv(s, assignment_wrapped_in_dv);
// versus
s = prn_eval_assign(s, raw_assignment_ptr);
```

You should get the same answer. We can handwave what that right answer is: as long as we do the same thing as `prn_eval_assign` would have, and THAT has good tests to confirm IT'S right, we're golden. This scales really easily to a variety of complex objects.

There's just one catch that was the lion's share of the timesuck: how do we test if two interpreter states are equal or not?

That's right. Up until this point, the test assertion framework has never needed to check if two IStates are equal or not. Which meant, side quest time.

The new test assertion framework is a massive improvement over its predecessor. You just have to implement a few things to allow `EQ(istate, a, b)` tests.

1. You need to have a `prn_drepr_istate` function, for debug-representing IState objects.  
2. You need to have a `prn_eq_istate_istate` function, and maybe one for not-equals (ne) too, which is just "do the equals function and then flip the answer".  
3. You need to tell the framework how to turn the first assertion arg (`istate`) into a real type name (`prn_istate_t`).  
4. You need to tell the framework which memory management scheme `istate` uses. In this case, `rc_release`/`rc_lend`, as opposed to `dv_release`/`dv_lend` or unmanaged value types.

These all need tests, so a lot of my day was just implementing this crap ^^ with test logic to prove stuff correct.

All that setup work just to be able to write this:

```c
prn_assign_t *assign = prn_mk_assign(PRN_ITEXT("foo"), PRN_DV_HTEXT("bar"));
prn_istate_t *as_rc = prn_eval_assign(prn_rc_lend(orig), prn_rc_lend(assign));
prn_istate_t *as_dv =
  prn_eval_dv(prn_rc_lend(orig), prn_conv(assign, assign, dv));
EQ(istate, as_rc, as_dv); // THE MONEY LINE
```

[https://git.sr.ht/~maddiem4/prone/tree/main/item/tests/eval.c](https://git.sr.ht/~maddiem4/prone/tree/main/item/tests/eval.c "https://git.sr.ht/~maddiem4/prone/tree/main/item/tests/eval.c")

The good news is, it's highly reusable, and I'll probably do test assertions on IStates all over the place now that it's convenient to do so. It was slow today because I was laying a solid foundation that will, eventually, make this project consistent in structure and easy to work on.

Thunk support is on the docket for tomorrow, and I expect to do all that and more for [#WorkWednesday](https://raphus.social/tags/WorkWednesday).