the first programming language I ever made
my interest in programming pretty much began in late 2016, at age ~12, when someone from my school spent some time teaching me a bit about Scratch. well, technically that wasn't my first ever experience with programming: I had messed a little with Scratch before, but I struggled to not lose my interest before I figured out how to do absolutely anything with it. and even before that, I had played quite a bit with a website called LiveCodeLab; I remember I would make thousands of overlapping rotating low-poly spheres and pretend it was a big spinning planet with lots of waves on it.
but anyway, it was only after that one person taught me Scratch that I developed a long-term obsession with programming and computer languages. I kept making lots of things with Scratch, started learning HTML and bits of Python, JavaScript, and Lua (for ComputerCraft), discovered esoteric programming languages such as brainfuck... and at some point during all that, I decided I should try making my very own programming language.
the programming language
the language was named SCCL, "Scratch Console Command Language". obviously, this language was implemented in Scratch. it was first published on March 22 2017, but apparently I privated it some time later, after presumably succumbing to a fear of people mocking me for making such a hideous abomination that hardly works. it has stayed privated ever since... until now! you can see it for yourself here.
here is the Hello World program in SCCL:
set_1_Hello world!_print_1
the first thing you'll notice is that all of the "terms" (per my own nomenclature from the time) are separated by underscores, instead of spaces. this is because I wanted strings to be able to contain spaces, and this was the least effort way I could think to achieve this. of course, this means that now you can't put underscores in strings, but hopefully you'll never have to do that. in hindsight it feels like it wouldn't have been that hard to implement a thing where arguments containing spaces must be enclosed with quotation marks (à la vurl, a language of almost-comparable rudimentarity). but at the time, figuring out how to split a string into a list of terms separated by a given character was just about the most challenging problem I had ever independently solved while programming, and I suppose I didn't want to complicate that any further.
another thing you may notice is that the Hello World program isn't just print_Hello world!
: you have to store the string in some numeric address and then print the value stored at that address. this is because SCCL was inspired by DDNC (DUO Decimal Numeric Code), the programming language created for Jack Eisenmann's DUO Decimal homebrew computer. back then I was very fixated on this computer and spent a lot of time messing with the emulator and writing programs in this charmingly rudimentary language. although, the one thing I never managed to figure out at the time was what "indirect addresses" (in other words, pointers) were supposed to do.
in DDNC, everything is numbers: every line of code is a numeric command followed by numeric arguments. there is just one command which lets you store a constant value into a variable, and then literally every other command exclusively takes in variable addresses as arguments. I suppose it's a little easier to parse (and makes the syntax of this number-only language less cumbersome) when you don't have to figure out whether each argument is supposed to be a constant value or a variable address, and when you don't have nested commands or expressions of any sort.
SCCL is similar, even down to the numeric variable addresses since Scratch only has lists and not dictionaries. although, it's not very hard to naively implement dictionaries by storing the keys in a separate list and linearly searching through that, but I guess I didn't figure that out either. or maybe I did, but decided I was fine with numbered variables and didn't bother. the main differences from DDNC are the human-readable command names (since we're not running on a 7-segment display anymore, are we), and the ability to store strings in variables, which comes for free since Scratch is a magical language where strings and numbers are virtually the same thing and you don't have to worry about allocating the memory to store those strings.
also, the entire program is on one line because Scratch's text input functionality only lets you type a single line. and don't try to write a program containing newlines and then paste it into the interpreter: Scratch will convert those into spaces, which will trip up the parser.
the perils of nesting
SCCL also allegedly has control flow mechanisms, namely if
and while
blocks terminated by an end
or wend
command respectively. for instance, this program asks you if 2 + 2 = 4, and tells you if you're right or wrong (and also doesn't handle the edge case of the user typing something that isn't "true" or "false"):
set_1_Does 2 + 2 = 4? (true/false)_prompt_2_1_if_2_set_1_Right!_print_1_end_not_2_2_if_2_set_1_Wrong!_print_1_end
...and if we may pretend for a moment that this language was more readable:
set 1 "Does 2 + 2 = 4? (true/false)" prompt 2 1 if 2 set 1 "Right!" print 1 end not 2 2 if 2 set 1 "Wrong!" print 1 end
I implemented these like so: if an if
or while
command's condition is false, it skips forwards through the code until it finds an end
or wend
term respectively, then jumps to immediately after it. when a wend
command is encountered, it searches backwards through the code until it finds a while
term, and jumps to it. there are at least a couple of flaws with this implementation. for one, it simply searches for any occurence at all of the name of the command it's looking for, regardless of whether it's actually a command. so your code may break if you just so happen to have the strings "end", "wend", or "while" as an argument.
the other flaw, which is the one I actually eventually realized at the time but didn't know how to solve, was that nested if
or while
statements do not work at all. because of course, those nested blocks will have their own end
/wend
commands, which the outer if
/while
block containing them will need to somehow know to ignore and not interpret as marking the end of its own block. funnily enough, about a month earlier I also wrote a brainfuck interpreter in Scratch which has the exact same flaw. I didn't realize that you didn't just have to skip to the next or previous bracket, you had to skip to the matching bracket.
nowadays I know the simple solution to solving this: when skipping forwards past a code block, keep a counter which stores how many levels of nested code blocks you're currently in (initialized to 1). increment it when a new block begins (when you see an if
/while
command), decrement it when a block ends (when you see an end
/wend
command). when the counter reaches 0, that's when you're finished skipping. the idea is similar when skipping backwards, except end
/wend
increments the counter and if
/while
decrements it. actually, I even know of a little trick to optimize while
loops: have the while
command push the current position in the code to a "loop stack" when entering its block, then have wend
pop from the loop stack and jump to that location, which is faster than needlessly searching backwards for that while
statement you've already seen before. but, I'm getting a bit ahead of myself.
miscellaneous funny aspects
the main mechanism for user input is the prompt
command, which requests a string from the user via Scratch's "ask (X) and wait" block. however, apparently there are commands for waiting for a keypress and storing which key was pressed (key
), and reading whether a given key is currenty being pressed... except, these commands only support the arrow keys and the spacebar. and reading the state of one of these keys is split across five separate commands (readup
, readdown
, readleft
, readright
, readspace
). I should have just made a single readkey
command which takes an argument, which would also be consistent with how the time
command works. but anyway, Scratch makes it so you sort of have to copy and paste code for every single key individually to get something like this to work, and I didn't really feel like doing that, so I figured those 5 keys would be good enough. although, apparently Scratch does let you shove variables into the "is (key) down?" block, which would at least make a readkey
command easier to implement, but I don't remember if this was a thing back in Scratch 2.0.
there are no equivalents for the DDNC commands involving "indirect addresses" (pointers) and lists, because as aforementioned, I couldn't figure out what "indirect addresses" were at the time. although, they technically aren't necessary, since you could theoretically use strings to store lists of values in some way, while accursedly accessing and mutating it with string operators like letter
and join
. but I never ended up writing any SCCL programs that needed to use lists anyways.
there is, however, the command sin
. no, I didn't know what the sine function was at the time. I only included it because DDNC included it.
commands
X
, Y
, and Z
represent numeric variable addresses. C
represents a constant value.
variable addresses start at 1. in the original implementation, the maximum variable address is 87 (of all numbers).
following Scratch behavior, the strings "true" and "false" are used as boolean values.
command | description |
---|---|
set_X_C |
store C into the variable X . |
copy_X_Y |
copy the value of X into Y . |
print_X |
print the value of X . |
add_X_Y_Z |
set X to Y + Z . |
sub_X_Y_Z |
set X to Y - Z . |
mul_X_Y_Z |
set X to Y * Z . |
div_X_Y_Z |
set X to Y / Z . |
mod_X_Y_Z |
set X to Y % Z . |
incr_X |
increment X by one. |
decr_X |
decrement X by one. |
round_X_Y |
set X to Y rounded to the nearest integer. |
floor_X_Y |
set X to Y rounded down to the nearest integer. |
ceiling_X_Y |
set X to Y rounded up to the nearest integer. |
random_X_Y_Z |
set X a random number between Y and Z . |
sin_X_Y |
set X to the sine of Y degrees. |
equal_X_Y_Z |
set X to "true" if Y equals Z , "false" otherwise. |
greater_X_Y_Z |
set X to "true" if Y is greater than Z , "false" otherwise.following Scratch behavior, this command may also compare strings based on lexicographical order. |
less_X_Y_Z |
set X to "true" if Y is less than Z , "false" otherwise.following Scratch behavior, this command may also compare strings based on lexicographical order. |
not_X_Y |
set X to "true" if Y does not equal "true", "false" otherwise. |
or_X_Y_Z |
set X to "true" if either Y or Z equal "true", "false" otherwise. |
and_X_Y_Z |
set X to "true" if both Y and Z equal "true", "false" otherwise. |
join_X_Y_Z |
set X to the contatenation of Y and Z . |
letter_X_Y_Z |
set X to the Y th character of Z (starting from 1). |
length_X_Y |
set X to the number of characters in Y . |
if_X |
begin a block of code that will execute if X is any value other than "false". |
end |
end an if block. |
while_X |
begin a block of code that will repeatedly execute until X is "false". |
wend |
end a while block. |
break |
immediately exit a while block. |
time_X_Y |
set X to the component of the current time given by Y .possible values for Y are: "second", "minute", "hour", "day" (of the week), "date" (of the month), "month", "year". |
wait_X |
wait for X seconds. |
readup_X |
set X to "true" if the up arrow key is pressed, "false" otherwise. |
readdown_X |
set X to "true" if the down arrow key is pressed, "false" otherwise. |
readleft_X |
set X to "true" if the left arrow key is pressed, "false" otherwise. |
readright_X |
set X to "true" if the right arrow key is pressed, "false" otherwise. |
readspace_X |
set X to "true" if the space key is pressed, "false" otherwise. |
key_X |
wait for a key press, then store the key code into X .key codes are "u" for up, "d" for down, "l" for left, "r" for right, "s" for space. |
prompt_X_Y |
print Y to the console, then prompt the user for text input and store the result into X . |
username_X |
store the user's Scratch username into X (or an empty string if the user is not logged in). |
clear |
clear the console. |
conclusion
this post has been sitting around unfinished for well over a month because I couldn't think of a way to shoehorn some sort of conclusion into this post. now I have realized that I don't really need to do that. so anyways that's it for this blog post, if you liked it make sure t