Package: s-interpreter
S Compiler & InterpreterTable of ContentsInstallationA Simple ExampleThe S CompilerCompiler UsageSlang FilesMAINSugar SectionsSugar TypesREPEAT StatementThe REPEAT TradeoffThe Numeric TypeThe Pitfalls of SugarsSugar Argument Edge CasesSugar Internal VariablesThe AlgorithmSugar ParsingCompiling the CodeSugar ExpansionThe S InterpreterInterpreter UsageThe S LanguageThe ModelVariablesLabelsInstructionsThe ProgramExampleSyntactic SugarsEncodingPairWiseListWiseInstruction EncodingProgram EncodingConventionsInstallationInstall (and update) usingpip:pipinstall-Us_interpreteror you can clone the repository usinggit:gitclonehttps://github.com/SamuelSill/s_interpreter.gitA Simple ExampleThe following is a program inSthat always returns the value ofX1:# Sugar for performing a non-conditional jump
> GOTO {Label L}
        Z <- Z + 1
        IF Z != 0 GOTO {L}

# Sugar for incrementing variable V1 by the value of V2
> {Variable V1} += {Variable V2}
        IF {V2} != 0 GOTO A
        GOTO E
    [A] {V2} <- {V2} - 1
        Z <- Z + 1
        IF {V2} != 0 GOTO A
    [B] Z <- Z - 1
        {V2} <- {V2} + 1
        {V1} <- {V1} + 1
        IF Z != 0 GOTO B
    [E] Y <- Y

# The program starts here
> MAIN
        Y += X1Compiling/Running the program:# Compile the program (in file main.slang) to binary file 'output.txt':s_compiler-fmain.slang-boutput.txt# Now 'output.txt' contains *pure* S language code, no sugars.# Run the interpreter on the binary file on input '42's_interpreter-boutput.txt42The console output:Output: 42You can learn more about theslangsyntaxhere.You can learn more about the basic syntax of theS Languagehere.The S CompilerThe S Compileris the central module of this repository.In short, it allows us to easily compileslangfiles to binaries that theS Interpretercan run.Compiler UsageTo compile a givenslangfile to a binary file, run the following command:s_compiler-f/slang/file/path-o/binary/file/pathYou could also pass a flag to print the encoding of the program instead, like so:s_compiler-f/slang/file/path--encodeBut be careful, as program encodings can grow incredibly large even with very few instructions,
so the compiler could throw an error instead.You could pass the flagprintto print the program to stdout like so:s_compiler-f/slang/file/path--printYou can also pass theverboseflag for additional prints regarding the compilation process like so:s_compiler-f/slang/file/path-o/binary/file/path--verboseYou can also provide the input program by passing its encoding (a number) instead of aslangfile like so:s_compiler-d{program-encoding}-o/binary/file/pathSlang FilesIn order to compileS Languagecode,
we write it in.slangfiles as a convention.Notice that the compiler is inherently case and whitespace insensitive.
The compiler only considers the line continuations between instructions,
and between separate words/variables in the same instruction..slangfiles are structured into sections, as follows:MAINTheMAINsection will contain the actual program you wish to compile.
Please note that it must appear at the end of the file.Take the following example:# This program always returns 'Y = X1' 
> MAIN
        IF X != 0 GOTO A
        Z <- Z + 1
        IF Z != 0 GOTO E
    [A] X <- X - 1
        Y <- Y + 1
        IF X != 0 GOTO AAgain, note that the indentation is only for ease of reading, and the compiler completely ignores it.Sugar SectionsThe previous example uses the pattern of theGOTO Lsyntactic sugar.What if we wanted to use this pattern multiple times?
We can define and use a syntactic sugar like so:> GOTO {Label L}
        Z <- Z + 1
        IF Z != 0 GOTO {L}

> MAIN
        IF X != 0 GOTO A
        GOTO E
    [A] X <- X - 1
        Y <- Y + 1
        IF X != 0 GOTO AA few details to notice:Notice that in the definition of the sugar, we specified the type ofL.
This sugar will only be used if we pass it with a valid label.Notice the usage of variableZinside theGOTO Lsugar.
When the compiler expands the usage of the sugar,
it replaces all internal variables used in the sugar to variables guaranteed to be unused anywhere else in the program.
The only exception to this rule is with I/O variables (Y/X1,X2, ...).
They can be manipulated internally from sugar code.A given sugar may only use other sugars that are defined before it in the file.Sugar definitions are allowed to overlap in terms of their usage patterns.
The sugar that will eventually be used in the compiled output is the first in the file that matches the string.Sugar TypesWhen defining sugars, we need to specify the type of their arguments.The supported sugar argument types are:Label- To only match labelsVariable- To only match variablesConst- To only match const numbers (e.g0, 1, 2, 3,...)Numeric- To match both variables and constsNote that you can overload sugar definitions only differing by the sugar arguments' types!While it seems pretty clear as to howLabel/Variableare used, how would one useConstin valid S Language?Well, there is only one way to use aConst, and that is with theREPEATstatement.REPEAT StatementSay for example we wanted to implement the sugar forV += 5.Well, we'll just write it like so:> {Variable V} += 5
      {V} <- {V} + 1
      {V} <- {V} + 1
      {V} <- {V} + 1
      {V} <- {V} + 1
      {V} <- {V} + 1But what if we wanted to implement the sugarV += kfor ANY const numberk?(Notice the difference here between a const numberkand a variableV.
While on a variable we can perform our regularSinstructions,
a const is a value that's knownIN COMPILE-TIMEand is not stored anywhere)In that case, we'd have to use theREPEATstatement:> {Variable V} += {Const K}
      {REPEAT K}
      {V} <- {V} + 1
      {END REPEAT}You might be thinking: "This is cheating! REPEAT is not a sugar nor is it a supported instruction!".Well, as explained before, syntactic sugars are only A CONVENTION
that we can all agree on when writingS Languagecode.TheREPEATkeyword is a convention for consts, that the lines shall be repeated as the size of the const.If you still think it's cheating, you're welcome to implementV += kas many times as you want for differentk's.The REPEAT TradeoffCurrently, the only supported functionality ofConsttypes is theREPEATstatement.
The initial plan was to implement another functionality called{REPEAT I TO K}.Similarly toREPEAT, it would repeat the blockKtimes, but it would also allow us to use another
compile-time variableI, that would represent the current block repetition index.
This way, we can repeat the block while still changing the behavior of how it generates based
on the value of const variableI.
We could even perform a nested{REPEAT I}based on const valueI.But we can go even further:
Given a constK, we can write aPythonfunction that generates different instructions based onK's value!
After all, sugars are a convention and therefore if we all agree that a given sugar is compiled by the output
of aPythonfunction, there should be no problem!But if we do that, we might as well write our code inPythonor some other high level language instead ofS.So that is why consts only support theREPEATkeyword.
Any additional keyword that we add will remove us of the challenge of writing inS, and defeat the purpose altogether.You can still do meta-programming inSif you want though. Just not with consts
(asSdoes not natively support them).The Numeric TypeWhile theConsttype helps reduce code repetition,
there could still be code repetition between const/variable implementations.Suppose we've already implemented sugars forV <- 0andV1 += V2,
and now we wish to implementV1 <- V2.The straightforward approach would be to implement it as follows:> {Variable V1} <- {Variable V2}
      {V1} <- 0
      {V1} += {V2}This implementation works fine, but now we decide to implement the same for consts.
So we implement the sugarV += K, and now we need to implementV <- K:> {Variable V} <- {Const K}
      {V} <- 0
      {V} += {K}But notice the code repetition between the two implementations! Yuck!Well,Numericcomes to the rescue:> {Variable V} <- {Numeric N}
      {V} <- 0
      {V} += {N}The numeric helps us create the two implementations at once!
This works as intended. When we use the sugar for consts, the compiler uses sugarV += Kto expand the second line,
and when we use the sugar for variables, it usesV1 += V2.You might think this is pretty minor, but imagine you wanted to implementV1 <- V2 + V3.
You'd have to implement the same sugar 4 times to allow for both const/variable usages.The Pitfalls of SugarsSugar Argument Edge CasesSuppose you've implemented the sugars{Variable V} <- {Const K}and{Variable V1} -= {Variable V2}correctly,
and you wish to use them to implement the sugar{Variable OUT} <- {Const K} - {Variable V}.Well, the straightforward approach is to implement it like so:> {Variable OUT} <- {Const K} - {Variable V}
        {OUT} <- {K}
        {OUT} -= {V}But there's an edge case here that needs to be addressed. Can you see it?What if we expand the lineZ <- 100 - Z?
It would expand to:Z <- 100
Z -= ZNo matter the initial value ofZ, the given lines will set it to0!So let's fix the case for whenVis the same variable asOUT:> {Variable OUT} <- {Const K} - {Variable V}
        Z <- {V}
        {OUT} <- {K}
        {OUT} -= ZNow you may argue that copying the value ofVtoZin the common case whereOUTis not the same variable asVis inefficient.That would be quite silly of you,
as it's funny to argue about efficiency in this language to begin with, but you'd still be right.Well we can add an exception to this case! We can overload the sugar like so:> {Variable OUT} <- {Const K} - {Variable OUT}
        Z <- {OUT}
        {OUT} <- {K}
        {OUT} -= Z

> {Variable OUT} <- {Const K} - {Variable V}
        {OUT} <- {K}
        {OUT} -= {V}The order hereMATTERS!
As explained previously, the sugar chosen to expand a given line will be thefirst in the filethat matches it.
If declared the other way around, the more general-case sugar will always take place,
and we'll be left with the same bug we started with.Sugar Internal VariablesSuppose you wished to initialize a given variableZto5in theMAINsection.You could simply write it asZ <- Z + 15times. After all,Zis initialized to0,
so we don't need to set it to0beforehand.But what about sugars?
You might think that it's safe to do the same with sugars, as the compiler guarantees
that the sugar's internal variables are not used anywhere else in the program.Well, while you may be right that the compiler takes care of us
with regard to the uniqueness of the variables, a problem may still occur
if the sugar is used inside a loop.Let's see an example.Suppose you've implemented{Variable V1} <- {Variable V2}correctly.
Let's look at the following code:> {Variable OUT} <- 1
      Z <- Z + 1
      {OUT} <- Z

> MAIN
  [A] Y <- 1
      X <- X - 1
      IF X != 0 GOTO AYou would assume for inputX = 2the value ofYwould be1.
But it would actually be2, as the lines thatY <- 1is expanded to work with the same variable,
whatever the compiler may choose it to be.Of course this issue would not be present if we were to write ourMAINsection like so:> MAIN
    Y <- 1
    Y <- 1This code will in fact always return 1, as the two lines expand to different lines,
hence with differentZvariables.Sugar local variables may seem to behave like static variables initialized to0at first,
but they're only shared between different 'calls' to the same occurrence of the sugar in our code.As a result of this, a guideline for writing your sugars should be to
make sure the internal variables you use are initialized to0,
either by actively zero-ing them at the start of the sugar, or by zero-ing them before exiting the sugar.You don't have to do that though if you don't actually care about the exact value of your variable (e.gGOTO L).The AlgorithmSugar ParsingFirstly, the compiler parses the different sugar sections and generates regex patterns
that would detect potential usages for each sugar.
It also validates the sugar's most basic syntax,
like making sure that for eachREPEATkeyword comes anEND REPEATeventually.Compiling The CodeThe compiler starts with theMAINsection.It goes over each instruction in the section:If it's a supportedSinstruction, just compile it as usualOtherwise, go over all known sugars in order, and find the first matching sugar.If there is none, the program does not compile.Otherwise, the compiler saves the sugar and its matching occurrence for later compilation.The compiler then goes over all sugars that were saved for later compilation and
expands each sugar in-place (using the previously compiled instructions to avoid name collision).Sugar ExpansionThe sugar expansion algorithm is probably where most of the brains of the compiler lies.It's a bit too intricate to be discussed here,
so it is recommended you read the algorithm in filecompiler.py, under theSyntacticSugar.compilefunction.The S InterpreterWhile theS Compilerdoes all the work of compiling.slangfiles to binaries,
the interpreter's purpose is only to run those compiled binaries.Interpreter UsageTo run the interpreter on a given binary file, run the following command:s_interpreter<x1-input><x2-input>...<xn-input>-b/binary/file/pathThe interpreter will print out the result of the binary on the given input (variableY).You could also pass an additional flag to print extra info about the run performed like so:s_interpreter<x1-input><x2-input>...<xn-input>-b/binary/file/path--run_infoThe S LanguageThe S Languageis amodel of computationthat is taught in courseTheory of ComputationatThe Academic College of Tel Aviv, Israel.The ModelVariablesThe main thing to note about theS Language, is that it only supports non-negative integers.The language supports three different variable types:Input Variables:X1/X,X2,X3...Temporary/Work Variables:Z1/Z,Z2,Z3...One Output Variable:Y1/YLabelsYou can mark instructions in theS Languagewith labels.The label names could only be of the following:A1/A,B1/B,C1/C,D1/D,E1/E,A2,B2, ...,E2,A3,B3, ...InstructionsThe S Languageonly supports 4 different instructions:Given any variableVand labelL:V <- V- A blank instruction (do nothing)V <- V + 1- IncrementVby 1V <- V - 1- IfVis0do nothing, Otherwise decrementVby 1IF V != 0 GOTO L- IfVisn't0, jump to the first occurrence of labelL(if nonexistent the program exits),
otherwise continue from the next instruction.The ProgramA program written in theS Languageis composed of a series of instructions, with or without labels.At the start of the program, all variables are initialized to0.Only the input variables (X1,X2, ...) are initialized with the user input.
All input variables that weren't given before running the program are also initialized to0.A given program may behave in one of two ways:Run infinitely, in which case it returns nothingFinish running, in which case ONLY the value of variableYis returnedThe program runs the instructions sequentially one after the other (unless a jump is performed),
and may exit by two ways:If it reaches the end of the programIf it performs a jump to a nonexistentlabel.ExampleLet's write a program that always returns the value of its first input (X1).Our first approach would be to write something like this:[A] X <- X - 1
    Y <- Y + 1
    IF X != 0 GOTO AThis program does the trick for all inputs except forX = 0.Let's fix the edge case:IF X != 0 GOTO A
    Z <- Z + 1
    IF Z != 0 GOTO E
[A] X <- X - 1
    Y <- Y + 1
    IF X != 0 GOTO ANotice that in order to jump unconditionally to non-existent labelE(and therefore exit),
we had to increment a temporary variable to make sure it's not0.Syntactic SugarsDue to the simplicity of theS Language, we might want to define conventions
for patterns that often appear in our code.Take for example the unconditional jump pattern to some labelL:Z <- Z + 1
IF Z != 0 GOTO LWe can agree that whenever someone writesGOTO Lin their program, it would mean that they wrote the above pattern.Of course theZvariable would have to be unique and unused anywhere in the program, and not hard-codedZ.
We wouldn't want our sugars to change the rest of the program's functionality.ThisDOES NOT MEAN thatGOTO Lis now a supported instruction in theS Language.
It's just a convention between us programmers.To compile a program that uses syntactic sugars,
we'd still need to expand them to their underlying instructions.The syntactic sugars will allow us to write higher level code while still respecting the language's restrictions.EncodingSince our language only supports integers,
we might want to encode structured data into integers so that we can process it.PairWiseIf we wish to encode the pair<a, b>wherea,bare non-negative integers, we follow the formula:$$\left< a, b \right> = 2^a(2b + 1) - 1$$ListWiseIf we wish to encode the list[e1, e2, ..., en],
where all elements are non-negative integers, we follow the formula:$$[e_1, e_2, ..., e_n] = \sum_{i=1}^n p_i^{e_i}$$Wherepiis the prime at positioni(e.g p1=2, p3=5)Notice that adding0to the end of the list does not change its encoding.
Therefore, if we wish to know the length of the list, we assume it won't end with0elements.Instruction EncodingEncoding Variables: If we wish to encode a given variable,
we just look at its index in the following sequence:Y,X1,Z1,X2,Z2,X3,Z3, ...For example,#Y= 1,#Z2= 5 etc.Encoding Labels: If we wish to encode a given label, we just look at its index in the following sequence:A1,B1,C1,D1,E1,A2,B2, ...For example,A1= 1,#C1= 3 etc.Encoding Instructions: If we wish to encode a given instruction (with or without label),
it is encoded as<a, <b, c>>, where:a:0if the instruction has no labelOtherwise the encoding of the labelb:0ifV <- V1ifV <- V + 12ifV <- V - 1(2+#L) ifIF V != 0 GOTO Lc:#V-1whereVis the variable in the instructionProgram EncodingIf we wish to encode the program:I1
I2
.
.
.
InWe just follow the formula:#[#I1, #I2, ..., #In] -1Notice that the encoding forY <- Yis 0.
This means that adding such instruction without a label to the end of a programwill notchange its encoding.
This is problematic, so as a convention, no program will end withY <- Ywithout a label.ConventionsIf the index of a variable/label is 1, we can omit it.No program will end with a non-labeledY <- Yinstruction.Although it's not mandatory, labelE(E1) is never used as a label of an instruction,
so that we can always jump to it if we wish to exit the program.