July 2nd, 2024

Exploring biphasic programming: a new approach in language design

Biphasic programming introduces new language design trends like Zig's "comptime" for compile-time execution, React Server Components for flexible rendering, and Winglang's phase-specific code for cloud applications.

Read original articleLink Icon
Exploring biphasic programming: a new approach in language design

Biphasic programming is a new trend in language design where code can run in two distinct phases, such as build time versus runtime or server-side versus client-side. Zig, a systems programming language, introduces "comptime" for compile-time execution of functions without adding a new domain-specific language. React Server Components (RSC) allow developers to choose where components are rendered, optimizing performance by rendering on the server or client as needed. Winglang focuses on cloud applications, using preflight code for defining infrastructure and inflight code for runtime interactions, enforcing phase-related invariants. These examples showcase how biphasic programming can address various challenges, from metaprogramming in Zig to optimizing frontend apps with RSC and modeling distributed programs in Wing. The distinction between phases in these languages offers unique capabilities, with potential for further exploration on how biphasic solutions overlap or differ, and whether existing languages can achieve similar functionality without dedicated features. Overall, biphasic programming presents a versatile approach to solving diverse programming problems across different domains.

Related

Understanding React Compiler

Understanding React Compiler

React's core architecture simplifies app development but can lead to performance issues. The React team introduced React Compiler to automate performance tuning by rewriting code using AST, memoization, and hook storage for optimization.

Understanding React Compiler

Understanding React Compiler

React's core architecture simplifies development but can lead to performance issues. The React team introduced the React Compiler to automate performance tuning by rewriting code. Transpilers like Babel convert JSX for efficiency. Compilers, transpilers, and optimizers analyze and produce equivalent code. React Compiler enhances functionality using Abstract Syntax Trees, memoization, and hook storage for optimized performance.

Zig-style generics are not well-suited for most languages

Zig-style generics are not well-suited for most languages

Zig-style generics, inspired by C++, are critiqued for limited universality. Zig's simplicity contrasts with Rust and Go's constraints. Metaprogramming praised for accessibility, but error messages and compiler support pose challenges. Limited type inference compared to Swift and Rust.

I Probably Hate Writing Code in Your Favorite Language

I Probably Hate Writing Code in Your Favorite Language

The author critiques popular programming languages like Python and Java, favoring Elixir and Haskell for immutability and functional programming benefits. They emphasize personal language preferences for hobby projects, not sparking conflict.

Improving Your Zig Language Server Experience

Improving Your Zig Language Server Experience

Enhance Zig Language Server (ZLS) by configuring it to run build scripts on save for immediate error display. Zig project progresses include faster builds, incremental compilation, and code intelligence. Support via Zig Software Foundation donations.

Link Icon 30 comments
By @chubot - 5 months
This is already known as "multi-stage programming" or "staged programming" -- I don't see a need for a new term

https://en.wikipedia.org/wiki/Multi-stage_programming

https://okmij.org/ftp/meta-programming/index.html

Comment from 2019 about it, which mentions Zig, Terra/Lua, Scala LMS, etc.:

https://news.ycombinator.com/item?id=19013437

We should also mention big data frameworks and ML frameworks like TensorFlow / Pytorch.

The "eager mode" that Chris Lattner wanted in Swift for ML and Mojo is to actually to get rid of the separation between the stage of creating a graph of operators (in Python, serially) and then evaluating the graph (on GPUs, in parallel).

And also CMake/Make and even autoconf/make and Bazel have stages -- "programming" a graph, and then executing it in parallel:

Language Design: Staged Execution Models - https://www.oilshell.org/blog/2021/04/build-ci-comments.html...

By @noelwelsh - 5 months
Like the other comment mentioned, this is staging.

> And compared to Lisps like Scheme and Racket which support hygenic macros, well, Zig doesn’t require everything to be a list.

This comment is a bit ignorant. Racket has the most advanced staging system of any language that I'm aware of. You can build languages in Racket with conventional yet extensible syntax: https://docs.racket-lang.org/rhombus/index.html Zig's metaprogramming facilities are very simple in comparison.

I think staging could be extremely useful in many application, and I wish it was better supported in mainstream langauges.

By @taliesinb - 5 months
The end-game is just dissolving any distinction between compile-time and run-time. Other examples of dichotomies that could be partially dissolved by similar kinds of universal acid:

* dynamic typing vs static typing, a continuum that JIT-ing and compiling attack from either end -- in some sense dynamically typed programs are ALSO statically typed -- with all function types are being dependent function types and all value types being sum types. After all, a term of a dependent sum, a dependent pair, is just a boxed value.

* monomorphisation vs polymorphism-via-vtables/interfaces/protocols, which trade roughly speaking instruction cache density for data cache density

* RC vs GC vs heap allocation via compiler-assisted proof of memory ownership relationships of how this is supposed to happen

* privileging the stack and instruction pointer rather than making this kind of transient program state a first-class data structure like any other, to enable implementing your own co-routines and whatever else. an analogous situation: Zig deciding that memory allocation should NOT be so privileged as to be an "invisible facility" one assumes is global.

* privileging pointers themselves as a global type constructor rather than as typeclasses. we could have pointer-using functions that transparently monomorphize in more efficient ways when you happen to know how many items you need and how they can be accessed, owned, allocated, and de-allocated. global heap pointers waste so much space.

Instead, one would have code for which it makes more or less sense to spend time optimizing in ways that privilege memory usage, execution efficiency, instruction density, clarity of denotational semantics, etc, etc, etc.

Currently, we have these weird siloed ways of doing certain kinds of privileging in certain languages with rather arbitrary boundaries for how far you can go. I hope one day we have languages that just dissolve all of this decision making and engineering into universal facilities in which the language can be anything you need it to be -- it's just a neutral substrate for expressing computation and how you want to produce machine artifacts that can be run in various ways.

Presumably a future language like this, if it ever exists, would descend from one of today's proof assistants.

By @pmontra - 5 months
A question to the author, about a choice in language design.

  // Import some libraries.
  bring s3;
If the keyword was the usual "import" there would be no need to explain what "bring" is. Or, if "bring" is so good, why not

  // Bring some libraries.

?
By @funcDropShadow - 5 months
This is also a special case of what MetaOCaml calls multi-stage programming. It does not only support two phases but arbitrary many. Some similar prototype also exists for some older Scala version. And Lisp and Forth obviously also support n-phases of computation.
By @warpspin - 5 months
He missed one of the earliest examples of "languages and frameworks that enable identical syntax to express computations executed in two distinct phases" - immediate words in Forth: https://www.forth.com/starting-forth/11-forth-compiler-defin...
By @cb321 - 5 months
Nim & D also have the compile-time function evaluation he mentions for Zig. Nim also has a full macro system wherein macros are written in Nim - just taking & producing ASTs. I've known people to refer to this/Julia macro systems as "homoiconic". Nim also has a javascript backend to enable similar same-syntax on client&server like his React & clojure examples.
By @cobbal - 5 months
Racket has a sophisticated system for dealing with phases in its macro system: https://docs.racket-lang.org/guide/phases.html I don't know if other schemes use a similar system.
By @thelittlenag - 5 months
I've been thinking similar thoughts recently since I've been exploring metaprogramming in Scala and how it can be extended to beyond the simplistic hygenic model it currently supports.

What I recently realized is that while compilers in the standard perspective process a language into an AST, do some transformations, and then output some kind of executable, from another perspective they are really no different than interpreters for a DSL.

There tends to be this big divide between what we call a compiler and what we call an interpreter. And we classify languages as being either interpreted or compiled.

But what I realized, as I'm sure many others have before me, is that that distinction is very thin.

What I mean is this: from a certain perspective a compiler is really just an interpreter for the meta language that encodes and hosts the compiled language. The meta-language directs the compiler, generally via statements, to synthesize blocks of code, create classes with particular shapes, and eventually write out certain files. These meta-languages don't support functions, or control flow, or variables, in fact they are entirely declarative languages. And yet they are the same as the normal language being compiled.

To a certain degree I think the biphasic model captures this distinction well. Our execution/compilation models for languages don't tend to capture and differentiate interpreter+script from os+compiled-binary very well. Or where they do they tend to make metaprogramming very difficult. I think finding a way to unify those notions will help languages if and when they add support for metaprogramming.

By @EricRiese - 5 months
Raku has this

https://docs.raku.org/language/phasers

It has many more than 2 phases.

Phasers is one of the ideas Raku takes as pretty core and really runs with it. So in addition to compile time programming, it has phasers for run time events like catching exceptions and one that's equivalent to the defer keyboard in several languages.

By @graypegg - 5 months
I wonder if something like Ruby could fit into this category too, even though there isn’t a clean line between the two phases. (I’m stretching the concept a bit heh)

The block inside of a class or module definition is executed first, and then the application can work on the resulting structure generated after that pass. Sorbet (a Ruby static typing library) uses this first-pass to generate its type metadata, without running application code. (I think by stubbing the class and module classes themselves?)

By @StiffFreeze9 - 5 months
Other "biphasic"-like aspects of programming languages and code:

- Documentation generated from inline code comments (Knuth's literate programming)

- Test code

We could expand to

- security (beyond perl taint)

- O(n) runtime and memory analysis

- parallelism or clustering

- latency budgets

And for those academically inclined, formal language semantics like https://en.wikipedia.org/wiki/Denotational_semantics versus operational and others..

By @gsuuon - 5 months
My toy language project is also built around multi-stage (though the way it's formed it's more like literate programming) and partly motivated by writing cloud-native applications. I played around with a sketch of this idea implemented using F# computation expressions[1] and partly implemented an Azure backend, at a high level it appears pretty similar to Winglang. When run at "comptime" / CLI, it spins up those resources if necessary and then produces artifacts via msbuild task for servers that run the "runtime" part of the code. The computation expression handles exposing a client and forming the ARM template based on the context. It gets around the inflight/preflight distinction by including the entire app (including provisioning stuff) in each runtime instance, so references outside of route scopes work (instance-globally, not app-globally).

Very excited for multi-stage - especially it's potential to provide very good LSP/diagnostics for library users (and authors). It's hard to provide good error messages from libraries for static errors that are hard to represent in the type system, so sometimes a library user sees vague/unrelated errors.

[1] https://github.com/gsuuon/kita/blob/d741c0519914369da9c89241...

By @kragen - 5 months
this 'biphasic programming' thing is item #9 in pg's list of 'what made lisp different' from 02001: https://paulgraham.com/diff.html

it's interesting to read this biphasic programming article in the context of pg's tendentious reading of programming language history

> Over time, the default language, embodied in a succession of popular languages, has gradually evolved toward Lisp. 1-5 are now widespread. 6 is starting to appear in the mainstream. Python has a form of 7, though there doesn't seem to be any syntax for it. 8, which (with 9) is what makes Lisp macros possible, is so far still unique to Lisp, perhaps because (a) it requires those parens, or something just as bad, and (b) if you add that final increment of power, you can no longer claim to have invented a new language, but only to have designed a new dialect of Lisp ; -)

it of course isn't absolutely unique to lisp; forth also has it

i think the academic concept of 'staged programming' https://scholar.google.com/scholar?cites=2747410401001453059... is a generalization of this, and partial evaluation is a very general way to blur the lines between compile time and run time

By @jalk - 5 months
"Biphasic programming" is also present in frameworks like Apache Spark, Tensorflow, build tools like Gradle and code-first workflow engines. Execution of the first phase generates a DAG of code to be executed later. IMO the hardest thing for newcomers is when phase 1 and phase 2 code is interleaved with no immediate clear boundaries, (phase 1 code resembles an internal DSL). The docs need to teach this early on to avoid confusion. A prime offender of this is SBT, with its (perhaps no longer true) 3 stage rocket, which is not really described in the docs (see https://www.lihaoyi.com/post/SowhatswrongwithSBT.html#too-ma...)
By @a1o - 5 months
I don't get the dismissal of C++, to me constexpr is exactly that! And now if we get reflection in C++26 it will be possible to do even more incredible things using it, but constexpr is already pretty good.
By @mikewarot - 5 months
Since we're going down the road of interesting ideas, let's add declarative programming to the mix

The Metamine language allowed for a magic equals := if I recall correctly, which had the effect of always updating the result anytime the assigned value changed for the rest of the life of the program. Mixing it with normal assignments and code made for some interesting capabilities.

By @m7zA3qHjoppS - 5 months
Another versions of this can be found in guix, called G-Expressions [0]. This is one of the reasons I like guix vs nix, as it allows you to write the package declarations and things like build steps [1] or even service definitions [2] in the same language.

[0] https://guix.gnu.org/manual/en/html_node/G_002dExpressions.h... [1] https://guix.gnu.org/manual/en/html_node/Build-Phases.html#i... [2] https://guix.gnu.org/manual/en/html_node/Shepherd-Services.h...

By @ithkuil - 5 months
Also interesting is the singeli language https://github.com/mlochbaum/Singeli/tree/master
By @JonChesterfield - 5 months
I'm pretty sure staged programming is a design mistake induced by the capabilities of computers in the ~70s. Needing to pay attention to which parts of a program have already been compiled and which haven't is totally orthogonal to whatever problem you're trying to solve with the computer. It's going to go the way of manual memory management.

The implementation shall be JIT compiled with a separate linter running in the editor for that is the right thing.

We aren't there yet but I believe it's where we'll end up.

By @zamalek - 5 months
For what its worth I like the function coloring Rust has, I don't believe compilation results should vary across separate runs. It's the same spirit as the rest of the language: highly predictable. The likes of diesel are very cool, but still amount to a big fat "yikes" from me.

I think the actual problem is the glacial pace of applying it, and the lack of support in trait impls (e.g. i32.min) and syntax. If it were applied to every pure fn+syntax it would probably cover a great deal of what Zig is doing.

By @AlexErrant - 5 months
Another example of biphasic programming is parser generators with DSLs for generating parsers, e.g. Tree Sitter or Lezer.
By @hbbio - 5 months
Funny to see the example of RSC in that context!

Multi-stage programming and distribution with the same syntax between clients and servers has been _the_ key feature of Opa (opalang.org) 15 years back. Funny because Opa was a key inspiration for React and its JSX syntax but it took a lot of time to match the rest of the features.

By @indyjo - 5 months
Would embedding code (which is executed by some other runtime, like SQL, shaders, compute kernels etc.) also be considered "biphasic" or "multi-stage" programming?
By @Svoka - 5 months
To be honest `comptime` seems excessive. Like, if something can be calculated at compile time, it should be. Why the extra keywords for that? Rust is mostly doing it already.
By @williamcotton - 5 months
I like the term biphasic! The prior terms for this with Javascript web development were "isomorphic" or "universal". I don't think these ever really caught on.

I've been rendering the same React components on the server and browser side for close to decade and I've come across some really good patterns that I don't really see anywhere else.

Here's the architectural pattern that I use for my own personal projects. For fun I've starting writing it in F# and using Fable to compile to JS:

https://fex-template.fly.dev

A foundational element is a port of express to the browser, aptly named browser express:

https://github.com/williamcotton/browser-express

With this you write not only biphasic UI components but also route handlers. In my opinion and through lots of experience with other React frameworks this is far superior to approaches taken by the mainstream frameworks and even how the React developers expect their tool to be used. One great side effect is that the site works the same with Javascript enabled. This also means the time to interaction is immediate.

It keeps a focus on the request itself with a mock HTTP request created from click and form post events in the browser. It properly architects around middleware that processes an incoming request and outgoing response, with parallel middleware for either the browser or server runtime. It uses web and browser native concepts like links and forms to handle user input instead of doubling the state handling of the browser with controlled forms in React. I can't help but notice that React is starting to move away from controlled forms. They have finally realized that this design was a mistake.

Because the code is written in this biphasic manner and the runtime context is injected it avoids any sort of conditionals around browser or server runtime. In my opinion it is a leaky abstraction to mark a file as "use client" or "use server".

Anyways, I enjoyed the article and I plan on using this term in practice!

By @JamesBarney - 5 months
As a Microsoft fanboy I have to list our their biphasic additions.

Linq. Have a a set of collection manipulation methods that could be run in c# or transformed in SQL.

Blazor. Have components that can run on the server, or in the browser, or several other rendering tactics.

By @z5h - 5 months
Take a look at term_expansion and goal_expansion in the Prologs that support them.
By @ceving - 5 months
> macro systems like those in C, C++, and Rust

Suggesting that the macros of C and Rust may be the same is an insane failure.

BTW: meta-programming means "code which generates code" and not "code which runs earlier than other code".