POP-2000, a lingua-franca POP-2 dialect
Written 2026-08-24
Devine (of Uxn fame) and I have been talking a lot about POP-2, a language from the 1970s originally used in the same circles as the first Lisp dialects.
Devine had been looking into writing a new assembler for Uxn in a high-level language to aid in bootstrapping a Uxn system, while I personally have always been interested in languages that would make for good "tiny compilers", and we came upon POP-2 as something that would make for a good lingua-franca between all of these tiny systems.
Tiny Compilers (or, why I don't exactly care for concatenative programming)
A bit of a crazy title, I know. Don't get me wrong, I love the concatenative programming paradigm- its way of forming new functions/constructs via juxtaposition and composition are really elegant. But something that I tried to make clear in the paper I wrote for ok is that I don't feel particularly drawn to concatenative languages for those features- rather, I like concatenative languages because they're ridiculously easy to write an interpreter/compiler for while (often, not always) allowing for some low-level control- at least, compared to the other languages that I like playing around with, like C or Odin.
Coming from someone who originally made conlangs (a LOT of conlangs) before they'd even begun to touch computer programming, I care more about the langdev-side of things than actually making software. Specifically, when I'd work on an idea for a programming language, I typically had the following goals in mind:
- As easy as possible to implement an interpreter/compiler for a given system, ideally using basically no external tools/libraries for parsing or code generation
- Fairly "usable" (rather vague and subjective, but effectively it "shouldn't look like an esolang", going by an "I know it when I see it" description)
- Minimal syntax
- Minimal set of features, essentially only the bare minimum you'd need to bootstrap the compiler/interpreter (which should be a goal basically as soon as you get the language working)
- Some low-level control/support for "systems programming", at least enough to where writing an operating system in the language should be feasible
- It should be hostable on a small system (normally I'd set my minimum expectations to that of ok, so ~16MiB of memory with room to spare, but Uxn or perhaps something like a 6502 would be even better)
Concatenative languages, and languages targeting small/low-level stack-based VMs in general, just happen to map really well to these sorts of constraints. I don't like concatenative languages because I think concatenation as a paradigm is inherently better, but rather, I like them because concatenation makes the implementation problem of creating a programming language delightfully small. The benefits of this naturally stretch further into the classic arguments in favor of these types of systems (i.e. permacomputing and whatnot).
A bit about POP-2
If you look at the goals for language design that I outlined earlier and compare them to the goals outlined in the original POP-2 Papers, they're basically identical:
Devine's POP-2 article lays out a good overview of a POP-2 dialect written to target Uxn, but to summarize, a minimal POP-2 implementation supports declaring multiple variables at once with vars x y z;, declaring functions with function myfunc a b; (followed by the function body and then an end), basic if/elseif/else control-flow, I/O with =>, as well as looping via labels and gotos. The neat thing about POP-2 is that values manipulate an underlying stack- from Devine's own article:
function sum x y;
x + y;
end
sum(5, 6) * 2;
Could just as easily be written this way, since the arguments to sum just get pushed onto the stack, and the value returned by sum also just gets pushed to the stack- that, and encountering an operator like + just looks ahead at the next element in the expression, pushes that to the stack, and then pops that and the next element off of the stack to add them together:
function sum x y;
x; + y;
end
5; 6; sum(); * 2;
With this inherently stack-based nature, adding just some basic memory management, support for different-width values for different systems, and a more universal I/O system would be all that's needed for a convenient, beginner-friendly language that could be made to run on tons of tiny systems- not as a replacement for those system's existing languages, but more as a social exercise by allowing newcomers to start off with a language that's both easy to learn and runs on tons of our small systems, while also being a language that's easy enough for us, the creators of these small systems, to implement a compiler for.
POP-2000, a modest proposal
POP-2000, (or POP2K, we aren't completely set on a name yet) is a standardized dialect of POP-2 with the goal of adding the support for systems-level stuff that we'd like on our small systems, while also being very clearly defined (if you pour through the reference section of the POP-2 papers, the grammar is defined but it's very hard to get through).
I've been working on it basically nonstop for the past few days, while talking with Devine (more like "pestering Devine incessantly") about what features should be supported or added over on the concatenative Discord server about the grammar, syntax, etc.
I hope to have more details online soon- again, this whole thing was nothing more than a "what-if" less than a week ago. I've just about completed a grammar for the language written in EBNF notation. I'll create a Git repo with the "standard" soon enough (debating not doing it on GitHub, what with the bad neglect and relationship with ICE), and as soon as that's created, I'll add it to this article. In the meantime though, I figured I'd write about what's in the works.
I also hope to write a kind of reference-implementation for the language's compiler in C, initially targeting ok, meaning soon our little VM is gonna have a high-level language!
POP-2000 EBNF grammar (work-in-progress)
For now, the full grammar I have so far in EBNF notation is as follows (expect changes in the future):
program = { element } ;
element = function
| vars
| imperative ;
function = "function" , identifier , [ params ] , ";" ,
[ function_body ] ,
"end" ;
const = "const"
vars = "vars" , var , { var } , ";" ;
var = identifier , [ ":" , ( integer | string ) ] ;
imperative = if
| ( statement_sequence , ";" ) ;
if = "if" , expression , "then" , [ body ] ,
{ "elseif" , expression , "then" , [ body ] } ,
[ "else" , [ body ] ] ,
"close" ;
body = { imperative | vars } , [ statement_sequence ] ;
statement_sequence = statement , { ";" , statement } ;
statement = goto
| labeled_statement
| expression_list ;
(* "return" is only allowed to occur WITHIN A FUNCTION BODY,
the parser should enforce this *)
goto = "goto" , identifier
| "return" ;
labeled_statement = label , { imperative } ;
label = identifier , ":" ;
expression_list = expression , { "," , expression } ;
expression = io ;
io = "=>" , primary
| assign ;
assign = "->" , primary
| apply ;
apply = "<>" , primary
| shift ;
shift = ( "<<" | ">>" ) , primary
| comparison ;
comparison = ( "<" | ">" | "=" | "!" ) , primary
| term ;
term = ( "+" | "-" ) , primary
| factor ;
factor = [ "*" | "/" | "%" ] , primary ;
primary = call
| reference
| quotation
| grouping
| index
| identifier
| literal ;
call = identifier , "(" , [ expression_list ] , ")" ;
reference = "#" , identifier ;
quotation = "@" , identifier ;
grouping = "(" , expression , ")" ;
index = "{" , expression , "}" ;
identifier = ( letter | "_" ) , { letter | digit | "_" } ;
literal = integer
| hexadecimal
| string ;
integer = digit , { digit } ;
hexadecimal = "0x" , hex_digit , { hex_digit } ;
string = "\"" , { string_char } , "\"" ;
string_char = ? any printable character except for double-quotes and backslash ?
| "\\\"" (* C-style escape for double quotes, with \" *)
| "\\" ; (* C-style escape for backslash *)
letter = "A" ... "Z"
| "a" ... "z" ;
digit = "0" ... "9" ;
hex_digit = digit
| "A" ... "F"
| "a" ... "f" ;