Promal does Advent of Code 2024 Day 1 & 2 on the Ultimate 64
Promal, or Programmer's Micro Application Language, is a programming language, library, and set of tools from about 1984. I think the last version for Commodore from Systems Management Associates, Inc. was in 1986, or at least that's the date on the latest documentation I can find. I redid days 1 and 2 from the advent of code (2024), but instead of Vision Basic I used Promal. Still using my Ultimate 64 at 48MHz, and this time I'm using its Software IEC "virtual drive" to speed up reading SEQuence files one line at a time. I've tried using this in the past but found it incompatible with basically everything -- you have to use a particular kernal, and the code can only use the standard kernal calls for accessing files. Thankfully Promal works fine once you disable its built-in fast loader. I did have one instance of file corruption, but I can't tell if it was Promal or this hyperspeed kernal and software IEC device. In any case, it loads things almost instantly, so that coupled with 48MHz and it's nothing but virtually instant load and compile times. So nice.
Getting the Data
This time, because of the virtual drive, it was no problem to load the data from sequence files. I converted them as explained before with the petcat
utility that comes with Vice.
petcat -text -w2 -o day2input.s.seq -- day2input.txt
I could have done like I did with Vision Basic and loaded the data lines (or Promal equivalents) directly into the source code, but Promal is a lot more restricted when it comes to lines of code and total memory available.
Speed
I used the same insertion sort algorithm as before. Promal says it compiles code to a p-code. And when you run a compiled program (with a .c extension) you run it within Promal's virtual machine, so to speak. The name they use is the Executive. I imagine it's just machine code calling Promal procedures in order to manage the memory and use common functions. In any case, I was pleasantly surprised that sorting was just as fast as Vision Basic. Promal texts say you won't be as fast as machine code for some things, but you'll be close enough. And programming in Promal vs assembly is night and day. Also, I didn't do any optimizing of the Promal code if there was any to do. It's more "high level" than Vision Basic and there is usually a standard way to do everything and there is no inline assembly. (Although you can easily call machine code routines from within your Promal code, even setting and reading registers before and after.)
Bigger Numbers
Promal definitely had the business use in mind when designing their language. It can handle larger numbers and part 1 of day 2, which before required I split out the sums, was no problem using a single real number type in Promal.
Promal Itself
Promal feels more modern than most languages I've tried on the Commodore 64. Outside of the restrictions of its platform, it makes very few compromises. There are very few unpolished edges, the documentation is excellent. While there are definitely additional features to add, it feels like the features that are there are very polished.
One thing I appreciated most coming from Vision Basic was how procedures and functions were scoped. Vision Basic has the local/global keywords, but they cut you off entirely from using global variables within local scopes. With no easy way to deal with global arrays in local scope I didn't see a point in using that for any but the most simple functions in Vision Basic. Promal uses a more modern approach and lets you access global variables within procedures which have their own local scope.
There is a script that runs each time the Executive boots up where you can put personal preferences or run any other Promal or machine code program you like. Code files are standard sequence files, so they are easy to edit in any editor you like and easy to pass around between modern PC and C64. You might imagine that such a file format is going to be larger than a tokenized BASIC PRG, but Promal makes up for it by letting you easily split programs between files. You can then include these files, either raw source or compiled source, easily within your programs. You can compile larger multi-file programs all from disk, and with the Software IEC drive it's super fast. Promal programs have less RAM to work with than Vision Basic files, although I obviously never ran into that limit with these simple programs.
There is a Promal tool that will package up your Promal compiled code into an "executable" so that it can be run outside of the Executive. I have not tried it, but the documentation says it basically bundles the Promal library and virtual machine together with your code.
I felt very productive with Promal. I ran into far fewer snags in my code than I did with Vision Basic. As I said, things were more polished. You give up some flexibility for a more solid, modern language and a better development experience overall. If only Promal were still actively developed like Vision Basic.
Day 1 Code
Day 1 is divided into 3 files, vars.s which has shared variable declarations, day1procs.s that has a bunch of functions in it I just didn't want to load each time I edited the main function, and day1.s that has the main function and also the similarity score function, which I could have moved to day1procs.s.
I printed these to the virtual printer in the U64 by redirecting the output of the
type
command to the printer withtype day1.s >p
. I used the ASCII output format which worked better with Promal's default use of the upper/lowercase font set.
day1.s
program day1
include library
include vars.s
real r
real startr
byte b
real row1[size]
real row2[size]
byte buf[17]
include day1procs.s
;-------------------
proc similarityScore
word row
word i
real score
begin
row=0
i=0
score=0.0
while row < size
output "#crow #w",row
i=0
while i < size
;output "#c i #w #7.0r #7.0r",i,row1[row],row2[i]
if row1[row] = row2[i]
score=score+row1[row]
i=i+1
row=row+1
output "#c#cpart 2 answer:#12.0r#c",score
end; similaraityScore
;****
begin
loadData
sortAndSum
similarityScore
put nl,"all done.",nl
end
vars.s
data word file="input.s,s"
con word size = 1000
day1procs.s
;---------
proc sort1
arg word i
word j
real t
begin
while i > 0
j = i-1
if row1[i] >= row1[j]
break
t = row1[j]
row1[j] = row1[i]
row1[i] = t
i = i-1
end ; sort1
;---------
proc sort2
arg word i
word j
real t
begin
while i > 0
j = i-1
if row2[i] >= row2[j]
break
t = row2[j]
row2[j] = row2[i]
row2[i] = t
i = i-1
end ; sort2
;------------
proc loadData
word ptr
word row
real v
byte i
begin
put nl,"loading...",nl
row=0
ptr=open(file)
while getlf(ptr, buf)
i=strreal(buf, #v)
row1[row]=v
i=strreal(buf+i, #v)
row2[row]=v
output "row #w #7.0r #7.0r#c",row,row1[row],row2[row]
row=row+1
end ; loadData
;--------------
proc sortAndSum
word row
real dis
real sum
begin
row=0
dis=0
sum=0
for row=0 to size-1
sort1 row
sort2 row
for row=0 to size-1
dis=abs(row1[row]-row2[row])
sum=sum+dis
output "#7.0r#7.0r",row1[row],row2[row]
output "=#12.0r#c",sum
output "#c#cpart 1 answer is #12.0r",sum
b=getkey
end ;sortAndSum
Day 2 Code
One thing you've probably noticed is that you have to define procedures and functions (and variables, of course) before you use them. Promal is a single-pass compiler, which makes it very quick, but does introduce this restriction.
program day2
include library
con byte cap=100
byte buf[25]
byte o[10]
byte r[10]
word safe
data byte dampener=1 ;0=part 1, 1=part 2
;---------------
proc printReport
word i
byte b
begin
i=0
put nl,"report: "
repeat
output "#w ",r[i]
i=i+1
until r[i]=cap
put nl
end; printReport
;--------
proc oToR
; copy the o array to the r array
arg byte skip
word a
word i
begin
a=0
i=0
for i=0 to 9
if i<>skip
r[a]=o[i]
a=a+1
end; oToR
;--------------
func byte check
word y
word x
word n
word a
begin
y=0
x=1
n=0
while r[x]<>cap
if r[n]=r[x]
return 1
if r[n]<r[x]
a=r[x]-r[n]
if r[n]>r[x]
a=r[n]-r[x]
if a>3
return 2
if n>=1 and r[y]<=r[n] and r[n]>r[x]
return 3
if n>=1 and r[y]>r[n] and r[n]<r[x]
return 4
n=n+1
x=x+1
if n>0
y=n-1
return 0
end; check
;---------------
func byte checkReport
byte e
word w
begin
e=0
w=0
if dampener=0
return check
while o[w]<>cap
oToR(w)
e=check
if e=0
break
w=w+1
return e
end; checkReport
;--------
proc loadReports
word file
byte b
byte index
word i
word w
begin
w=0
b=0
safe=0
file=open("day2input.s,s") ;d2example.s
while getlf(file, buf,79)
i=0
b=0
index=0
repeat
index=index+b
b=strval(buf+index,#w)
if b<>0
o[i]=w:<
i=i+1
else if i<>0
o[i]=cap
oToR(-1)
;printReport
if checkReport=0
safe=safe+1
until b=0
end; loadReports
;--------
proc main
byte b
word i
word w
begin
loadReports
output "#csafe reports = #w#c",safe
end; main
;****
begin
safe=0
main
end
Job Scripts
One more thing I'll share is that Promal has this idea of "jobs", which are just script files that can have any number of Executive commands in them and even chain to other job scripts. This is how the startup script works and it's also how I created a run.j
script that opens a file for editing then automatically compiles and runs it after the editor closes. This is suggested by the documentation.
run .j
The unload
command gets everything out of memory so there is maximum available for editing/compiling, etc. The map
command gives a printout of how much memory is being used and is free and what is using memory. Very handy. The £1
references the first command-line argument passed to this script. So running job run.j day2.s
would substitute day2.s
wherever you find £1
.
; run a program
unload
edit £1.s
delete £1.c ;delete so no confirmation
unload £1
map
compile £1.s
£1
bootscript.j
This is my startup script, which puts the Ultimate 64 into turbo mode, sets a block cursor (because it blinks too fast when in turbo mode), has some test code where I set the date (which is uses to date-stamp compiled files), then I set my color theme and set the F5 key to run my job script. So starting my edit-compile-test loop was as easy as F5, RETURN.
set d030 ff ;turbo enable bit
set cff 80 ;block cursor
set 0c12 1b ;set month
set 0c13 0c ;set day
set 0c14 18 ;set year part 1 (24)
set 0c15 14 ;set year part 2 (20)
dump 0c12 0c15
set d020 0 ;border color
color 3 0 ;text and background color
fkey 5 "job run.j day2"
Conclusion
So, I knew I wasn't going to move on to day 3 this year, but Vision Basic kind of led me to research other languages which brought me to Promal which was too intriguing not to do something with. So why not redo day 1 and 2, which I knew could run on the C64. It turns out I really liked it. It took a few days to get through all the documentation (it's a lot), but after that it was super quick to re-implement my code in the Promal way. I'm glad I did. It makes native development on the C64 a lot of fun.