De auteur stelt samen met Devine POP-2000 (of POP2K) voor, een gestandaardiseerd dialect van de programmeertaal POP-2 uit de jaren 70. Het doel is om een 'tiny compiler' te creëren die eenvoudig te implementeren is op kleine systemen (zoals Uxn, 6502 of ok) zonder afhankelijk te zijn van complexe externe tools.
De kern van het project is gebaseerd op de volgende inzichten:
- Implementatiegemak: De auteur heeft een voorkeur voor concatenatieve en stack-gebaseerde talen, niet per se vanwege het paradigma, maar omdat de implementatie van de compiler of interpreter hierdoor extreem eenvoudig is.
- Kenmerken van POP-2: De taal is inherent stack-gebaseerd, wat zorgt voor een goede balans tussen low-level controle en bruikbaarheid voor beginners.
- Standaardisatie: POP-2000 beoogt de vaak onduidelijke grammatica van de originele POP-2 te formaliseren via een EBNF-notatie en essentiële systeemfuncties toe te voegen.
Het project bevindt zich in een beginfase; er is inmiddels een concept-grammatica beschikbaar en er wordt gewerkt aan een referentie-implementatie van de compiler in C, specifiek gericht op de ok VM.
POP-2000: een lingua franca POP-2-dialect
Devine (bekend van Uxn) en ik hebben veel gesproken over POP-2, een taal uit de jaren 70 die oorspronkelijk werd gebruikt in dezelfde kringen als de eerste Lisp-dialecten.
Devine was bezig met het schrijven van een nieuwe assembler voor Uxn in een hogere taal om het bootstrappen van een Uxn-systeem te vergemakkelijken. Ikzelf ben altijd geïnteresseerd geweest in talen die geschikt zijn als "tiny compilers". We kwamen tot de conclusie dat POP-2 een uitstekende lingua franca zou kunnen vormen tussen al deze kleine systemen.
Kleine compilers (of waarom ik niet precies van concatenatieve programmering hou)
Een nogal provocerende titel, ik weet het. Begrijp me niet verkeerd: ik hou van het concatenatieve programmeringsparadigma; de manier waarop nieuwe functies en constructies worden gevormd via juxtapositie en compositie is zeer elegant. Maar, zoals ik probeerde duidelijk te maken in het artikel dat ik schreef voor ok, voel ik me niet specifiek aangetrokken tot concatenatieve talen vanwege die kenmerken. In plaats daarvan hou ik ervan omdat het belachelijk eenvoudig is om een interpreter of compiler voor deze talen te schrijven, terwijl ze (vaak, maar niet altijd) een zekere mate van low-level controle bieden — althans, vergeleken met andere talen waar ik graag mee experimenteer, zoals C of Odin.
Als iemand die oorspronkelijk veel kunsttalen (conlangs) maakte voordat ik überhaupt aan computerprogrammering begon, ben ik meer geïnteresseerd in de ontwikkeling van de taal (langdev) dan in het daadwerkelijk bouwen van software. Specifiek wanneer ik werkte aan een idee voor een programmeertaal, had ik doorgaans de volgende doelen voor ogen:
- Eenvoudige implementatie: Een interpreter of compiler moet zo gemakkelijk mogelijk te implementeren zijn voor een bepaald systeem, idealiter zonder gebruik te maken van externe tools of bibliotheken voor parsing of codegeneratie.
- Bruikbaarheid: De taal moet redelijk "bruikbaar" zijn. Dit is vaag en subjectief, maar effectief betekent het dat het niet mag aanvoelen als een esolang (een exotische taal).
- Minimale syntaxis.
- Minimale feature-set: Alleen het absolute minimum dat nodig is om de compiler of interpreter te bootstrappen (wat een doel moet zijn zodra de taal werkt).
- Low-level controle: Ondersteuning voor "systems programming", in ieder geval voldoende om het schrijven van een besturingssysteem in de taal haalbaar te maken.
- Hostbaar op kleine systemen: De taal moet kunnen draaien op kleine systemen. Normaal gesproken hanteer ik de specificaties van ok als minimum (~16MiB geheugen met wat ruimte over), maar Uxn of iets als een 6502 zou nog beter zijn.
Concatenatieve talen, en talen die gericht zijn op kleine/low-level stack-gebaseerde VM's in het algemeen, sluiten toevallig heel goed aan bij dit soort beperkingen. Ik hou niet van concatenatieve talen omdat ik denk dat concatenatie als paradigma inherent superieur is, maar omdat concatenatie het implementatieprobleem van het creëren van een programmeertaal heerlijk klein maakt. De voordelen hiervan strekken zich natuurlijk verder uit naar de klassieke argumenten in het voordeel van dit type systemen (zoals permacomputing).
Over POP-2
Als je de eerder beschreven doelen voor taalontwerp vergelijkt met de doelen in de originele POP-2-papers, zijn ze vrijwel identiek.
Het artikel van Devine over POP-2 geeft een goed overzicht van een POP-2-dialect dat is geschreven voor Uxn. Samengevat ondersteunt een minimale POP-2-implementatie het volgende:
- Het tegelijkertijd declareren van meerdere variabelen met
vars x y z;.
- Het declareren van functies met
function myfunc a b; (gevolgd door het functiebody en een end).
- Basis control-flow met
if/elseif/else.
- I/O via
=>.
- Looping via labels en
goto's.
Het bijzondere aan POP-2 is dat waarden een onderliggende stack manipuleren. Uit het artikel van Devine:
function sum x y;
x + y;
end
sum(5, 6) * 2;
Dit zou net zo goed op de volgende manier geschreven kunnen worden, aangezien de argumenten voor sum simpelweg op de stack worden geplaatst, en de waarde die door sum wordt teruggegeven eveneens op de stack belandt. Bovendien kijkt een operator zoals + simpelweg naar het volgende element in de expressie, plaatst dat op de stack, en plopt vervolgens dat element en het volgende element van de stack om ze bij elkaar op te tellen:
function sum x y;
x; + y;
end
5; 6; sum(); * 2;
Door deze inherent stack-gebaseerde natuur zou het toevoegen van basisgeheugenbeheer, ondersteuning voor waarden van verschillende breedtes voor verschillende systemen en een universeler I/O-systeem voldoende zijn voor een handige, beginnersvriendelijke taal. Deze taal zou op talloze kleine systemen kunnen draaien — niet als vervanging voor de bestaande talen van die systemen, maar als een sociale oefening. Het stelt nieuwkomers in staat te beginnen met een taal die zowel gemakkelijk te leren is als draait op veel van onze kleine systemen, terwijl het voor ons, de makers van deze systemen, eenvoudig genoeg is om een compiler voor te implementeren.
POP-2000: een bescheiden voorstel
POP-2000 (of POP2K, we hebben nog geen definitieve naam) is een gestandaardiseerd dialect van POP-2. Het doel is om ondersteuning toe te voegen voor systeemfuncties die we op onze kleine systemen willen, terwijl de taal zeer duidelijk wordt gedefinieerd (als je door de referentiesectie van de POP-2-papers bladert, is de grammatica wel gedefinieerd, maar zeer moeilijk te doorgronden).
Ik heb de afgelopen dagen vrijwel non-stop hieraan gewerkt en heb uitgebreid overlegd met Devine op de concatenatieve Discord-server over welke functies ondersteund of toegevoegd moeten worden wat betreft grammatica, syntaxis, etc.
Ik hoop binnenkort meer details online te plaatsen — dit hele project was minder dan een week geleden nog niets meer dan een "wat als"-gedachte. Ik heb inmiddels bijna een grammatica voor de taal voltooid, geschreven in EBNF-notatie. Ik zal binnenkort een Git-repo met de "standaard" aanmaken (ik overweeg om dit niet op GitHub te doen, gezien de slechte relatie met ICE). Zodra die er is, voeg ik deze toe aan dit artikel. In de tussentijd leek het me goed om te beschrijven waar we mee bezig zijn.
Daarnaast hoop ik een referentie-implementatie voor de compiler van de taal te schrijven in C, in eerste instantie gericht op ok. Dat betekent dat onze kleine VM binnenkort een taal van een hoger niveau zal hebben!
POP-2000 EBNF-grammatica (werk in uitvoering)
Voor nu is de volledige grammatica in EBNF-notatie als volgt (verwacht wijzigingen in de toekomst):
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" ;
POP-2000: een lingua franca POP-2-dialect
Devine (bekend van Uxn) en ik hebben veel gesproken over POP-2, een taal uit de jaren 70 die oorspronkelijk werd gebruikt in dezelfde kringen als de eerste Lisp-dialecten.
Devine was bezig met het schrijven van een nieuwe assembler voor Uxn in een hogere taal om het bootstrappen van een Uxn-systeem te vergemakkelijken. Ikzelf ben altijd geïnteresseerd geweest in talen die geschikt zijn als "tiny compilers". We kwamen tot de conclusie dat POP-2 een uitstekende lingua franca zou kunnen vormen tussen al deze kleine systemen.
Kleine compilers (of waarom ik niet precies van concatenatieve programmering hou)
Een nogal provocerende titel, ik weet het. Begrijp me niet verkeerd: ik hou van het concatenatieve programmeringsparadigma; de manier waarop nieuwe functies en constructies worden gevormd via juxtapositie en compositie is zeer elegant. Maar, zoals ik probeerde duidelijk te maken in het artikel dat ik schreef voor ok, voel ik me niet specifiek aangetrokken tot concatenatieve talen vanwege die kenmerken. In plaats daarvan hou ik ervan omdat het belachelijk eenvoudig is om een interpreter of compiler voor deze talen te schrijven, terwijl ze (vaak, maar niet altijd) een zekere mate van low-level controle bieden — althans, vergeleken met andere talen waar ik graag mee experimenteer, zoals C of Odin.
Als iemand die oorspronkelijk veel kunsttalen (conlangs) maakte voordat ik überhaupt aan computerprogrammering begon, ben ik meer geïnteresseerd in de ontwikkeling van de taal (langdev) dan in het daadwerkelijk bouwen van software. Specifiek wanneer ik werkte aan een idee voor een programmeertaal, had ik doorgaans de volgende doelen voor ogen:
- Eenvoudige implementatie: Een interpreter of compiler moet zo gemakkelijk mogelijk te implementeren zijn voor een bepaald systeem, idealiter zonder gebruik te maken van externe tools of bibliotheken voor parsing of codegeneratie.
- Bruikbaarheid: De taal moet redelijk "bruikbaar" zijn. Dit is vaag en subjectief, maar effectief betekent het dat het niet mag aanvoelen als een esolang (een exotische taal).
- Minimale syntaxis.
- Minimale feature-set: Alleen het absolute minimum dat nodig is om de compiler of interpreter te bootstrappen (wat een doel moet zijn zodra de taal werkt).
- Low-level controle: Ondersteuning voor "systems programming", in ieder geval voldoende om het schrijven van een besturingssysteem in de taal haalbaar te maken.
- Hostbaar op kleine systemen: De taal moet kunnen draaien op kleine systemen. Normaal gesproken hanteer ik de specificaties van ok als minimum (~16MiB geheugen met wat ruimte over), maar Uxn of iets als een 6502 zou nog beter zijn.
Concatenatieve talen, en talen die gericht zijn op kleine/low-level stack-gebaseerde VM's in het algemeen, sluiten toevallig heel goed aan bij dit soort beperkingen. Ik hou niet van concatenatieve talen omdat ik denk dat concatenatie als paradigma inherent superieur is, maar omdat concatenatie het implementatieprobleem van het creëren van een programmeertaal heerlijk klein maakt. De voordelen hiervan strekken zich natuurlijk verder uit naar de klassieke argumenten in het voordeel van dit type systemen (zoals permacomputing).
Over POP-2
Als je de eerder beschreven doelen voor taalontwerp vergelijkt met de doelen in de originele POP-2-papers, zijn ze vrijwel identiek.
Het artikel van Devine over POP-2 geeft een goed overzicht van een POP-2-dialect dat is geschreven voor Uxn. Samengevat ondersteunt een minimale POP-2-implementatie het volgende:
- Het tegelijkertijd declareren van meerdere variabelen met
vars x y z;.
- Het declareren van functies met
function myfunc a b; (gevolgd door het functiebody en een end).
- Basis control-flow met
if/elseif/else.
- I/O via
=>.
- Looping via labels en
goto's.
Het bijzondere aan POP-2 is dat waarden een onderliggende stack manipuleren. Uit het artikel van Devine:
function sum x y;
x + y;
end
sum(5, 6) * 2;
Dit zou net zo goed op de volgende manier geschreven kunnen worden, aangezien de argumenten voor sum simpelweg op de stack worden geplaatst, en de waarde die door sum wordt teruggegeven eveneens op de stack belandt. Bovendien kijkt een operator zoals + simpelweg naar het volgende element in de expressie, plaatst dat op de stack, en plopt vervolgens dat element en het volgende element van de stack om ze bij elkaar op te tellen:
function sum x y;
x; + y;
end
5; 6; sum(); * 2;
Door deze inherent stack-gebaseerde natuur zou het toevoegen van basisgeheugenbeheer, ondersteuning voor waarden van verschillende breedtes voor verschillende systemen en een universeler I/O-systeem voldoende zijn voor een handige, beginnersvriendelijke taal. Deze taal zou op talloze kleine systemen kunnen draaien — niet als vervanging voor de bestaande talen van die systemen, maar als een sociale oefening. Het stelt nieuwkomers in staat te beginnen met een taal die zowel gemakkelijk te leren is als draait op veel van onze kleine systemen, terwijl het voor ons, de makers van deze systemen, eenvoudig genoeg is om een compiler voor te implementeren.
POP-2000: een bescheiden voorstel
POP-2000 (of POP2K, we hebben nog geen definitieve naam) is een gestandaardiseerd dialect van POP-2. Het doel is om ondersteuning toe te voegen voor systeemfuncties die we op onze kleine systemen willen, terwijl de taal zeer duidelijk wordt gedefinieerd (als je door de referentiesectie van de POP-2-papers bladert, is de grammatica wel gedefinieerd, maar zeer moeilijk te doorgronden).
Ik heb de afgelopen dagen vrijwel non-stop hieraan gewerkt en heb uitgebreid overlegd met Devine op de concatenatieve Discord-server over welke functies ondersteund of toegevoegd moeten worden wat betreft grammatica, syntaxis, etc.
Ik hoop binnenkort meer details online te plaatsen — dit hele project was minder dan een week geleden nog niets meer dan een "wat als"-gedachte. Ik heb inmiddels bijna een grammatica voor de taal voltooid, geschreven in EBNF-notatie. Ik zal binnenkort een Git-repo met de "standaard" aanmaken (ik overweeg om dit niet op GitHub te doen, gezien de slechte relatie met ICE). Zodra die er is, voeg ik deze toe aan dit artikel. In de tussentijd leek het me goed om te beschrijven waar we mee bezig zijn.
Daarnaast hoop ik een referentie-implementatie voor de compiler van de taal te schrijven in C, in eerste instantie gericht op ok. Dat betekent dat onze kleine VM binnenkort een taal van een hoger niveau zal hebben!
POP-2000 EBNF-grammatica (werk in uitvoering)
Voor nu is de volledige grammatica in EBNF-notatie als volgt (verwacht wijzigingen in de toekomst):
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" ;