Delta Language

17 Sep 2022 ideas

A language where change is the primary thing modeled. It embraces change. Logic and data change all the time in the course of the lifetime of an application, but we still work with snapshots. Delta instead reifies the concept of change into the language.

Exploration of what change means

Some of these sections may contradict each other as the exploration of ideas happens. You have been warned.

Is change at the level of source lines of code?

To add 2 numbers in a C-like programming language, we’d write:

int sum(int x, int y){
    return x+y;
}

If we want to now add 3 numbers, we need to a) add a parameter, say z, and b) update the return expression to x+y+z. This could be done in multiple ways:

  1. As a text change in the source (which is what happens today in the IDE or editor)
  2. As a structural change to the source AST in a structural editor (happens today too, but structural editors are not heavily adopted)

To do the second option as part of the language tooling, it would have to do these steps:

add a param z to the function declaration
modify the "x+y" expression to add a "+z" subexpression

i.e., generate code similar to below:

add_param(sum,z, int);
cursor=update_expr(expr345);
rn = cursor.getRootNode(); // assuming + is the root node

nn = cursor.newNode({type:OPERATOR, value: "+"});
nr = cursor.newNode({type:VARIABLE, value: "z"});

nn.setLeftChild(rn);
nn.setRightChild(nr);

cursor.setRootNode(nn);

…which is painful and pointless.

So there must be a level of granularity at which change needs to be declared and the tool “just does it” and a level below where its easier for humans to do it.

instead, what if we required every change to be described briefly as part of the change? Something like:

changed foo, added z, reason "foo should now add 3 numbers not two"

How can this be enforced?

  1. By checking timestamps of source and built files? easy to game, but might be the simplest. But unless each function is stored in a separate file, its not easy to map a change directly.
  2. By checking against a version controlled source: This prevents “local” changes from ever being captured, but maybe thats ok. But also: compiler is now tied to version control. That might not be a bad thing for a language that is focused on change control!
  3. All change should start with the reason, and that should be reified into the edit session (or the commit event)

Maybe the unit of change is the function, not the logic within. That is handled via a change statement that’s required for every function. Also, maybe each function gets its own file.

Starting change with a reason for change

Expanding on the bullet above, what if any change were started with the reason to begin with? This could be used to setup the “task” in an editing session or the commit reason when code is checked in. The reason could be text, or a link to a ticket for a feature or bug fix.

What if analysis support

More interesting are use cases like evolution of the program’s interface. What if a parameter needs to be changed in type or be made required? Could the language tooling provide impact analysis?

All parameters are non-primitive structs/classes

One issue with changing interfaces is that changes in method parameter lists are especially difficult for clients to adopt. Instead, what if the language allowed only non-primitive values as parameters? that way all request and responses are boxed, and the interfaces are not as much a cause for change management.

All logic is execution environment agnostic (but not necessarily stateless)

In order to be able to use any piece of logic portably, it should make the least assumptions about its environment. So what if the things in the environment that a function depends on are explicitly stated up-front? that is, each function declares its dependencies explicitly, which could be libraries, values (in and out), env vars, etc. This also strengthens the case for each function to be the unit of source storage, i.e., file.

Extracting logic from one function to another, merging functions, etc

Another kind of change is when logic needs to be extracted into another function, or when two (or more) functions need to be merged into one. This is where knowledge of the syntax will matter. IDEs already support this, but imagine a compiler command that says “Extract lines 15-25 to a new function” and the compiler does that. That is, the compiler replaces the call site with the new name, annotates the new function with dependencies and local variables, any values that are used across the boundary become out parameters. This requires full program transformation capability.

Upgrading to a new language version is automated (mostly)

This was started by Facebook’s Hack language where migration to a new version of the language is handled as automatically as possible. The idea is that there’s a source-to-source translation possible so that old code is automatically upgraded to the new version.

There are no unimportant changes

The above ideas imply that sometimes the actual changed aspects might be too small to have perfect information of. We could also feel like may “editing versions” of code need not be considered from the compiler’s perspective. I posit that maybe it should. If there’s a delta between this version and the previous, it should see some explanation in terms of requirements, design, code or testing. So maybe all changes matter and we should not take “small” changes lightly.

Consequences of requirements on implementation

If the compiler and language toolbench have to support these features, they must:

  1. …be able to do full source comprehension and diff across versions
  2. …still support “opaque” changes that are just described, not actually diffed/analyzed
  3. …have the function as unit of code storage, change and dependency declaration.
  4. …be able to do source to source transformations
© 2024 Vinod KD