commit 3c7376cca68b6aa99f2d2eddd11bcf1224bcf632 Author: maride Date: Sun Dec 8 17:55:49 2019 +0100 Init commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..9029327 --- /dev/null +++ b/README.md @@ -0,0 +1,154 @@ +# MeXiCo + +MeXiCo is an esoteric programming language, and a *compiler* for the language with the same name, compiling source code to [DNS MX records](https://en.m.wikipedia.org/wiki/MX_record). This also explains the name. + +It's greatly inspired by [blinry](https://morr.cc)'s [legit](https://github.com/blinry/legit) project and [brainfuck](https://en.wikipedia.org/wiki/Brainfuck), and is written in [go](https://golang.org). +This project was born and written on a boring railroad trip from Hamburg to Düsseldorf. + +## Machine specification + +Like some other esoteric programming languages, a MeXiCo machine has a storage of unlimited size, also called *infinite tape*, and a first-in-last-out stack. It's possible to read from and write to the tape with a movable *head*. This makes MeXiCo turing-complete. + +## Design + +As stated above, the source code of a MeXiCo program completely resides in MX records. The compiler ensures that generated MX records are RFC-conform. This means, it is possible to deliver *MeXiCo* source code over a DNS server of your choice, and, using the [time-to-live](https://en.wikipedia.org/wiki/Time_to_live) value, cache source code on a DNS resolver of your choice. + +Like in the old [BASIC](https://en.wikipedia.org/wiki/BASIC) days, source code *lines* are defined by a number at the beginning of a line, sitting in the [Priority](https://en.wikipedia.org/wiki/MX_record#MX_preference,_distance,_and_priority) value of the MX record. Lines which are not filled out are skipped. As a short explanation: + +``` +someprogram.esolang.mil IN MX 10 +someprogram.esolang.mil IN MX 20 +someprogram.esolang.mil IN MX 30 +``` + +Due to the fact that the payload of a MX records needs to be a [FQDN](https://en.wikipedia.org/wiki/FQDN), every command is represented as a subdomain of the domain `mexico.invalid.`, which is obviously non-existent. If a command contains spaces, for example if they carry an argument (`push 5`), every space is replaced by a minus sign. + +## Instructions + +Below, you can find the instructions used in the MX records. + +| Command | Involves | Consumes Stack | Pushes to Stack | Description | +| --- | --- | --- | --- | --- | +| `left` | Tape Head | 0 | 0 | Moves the tape head one cell to the left | +| `right` | Tape Head | 0 | 0 | Moves the tape head one cell to the right | +| `pusht` | Tape, Stack | 0 | 1 | Reads the current cell value and pushes it on top of the stack | +| `push n` | Stack | 0 | 1 | Pushes the value `n` to the stack | +| `pop` | Tape, Stack | 1 | 0 | Pops top stack value to the current cell | +| `dup` | Stack | 1 | 2 | Duplicates the topmost stack value | +| `del` | Stack | 1 | 0 | Deletes the topmost stack value, ignoring its value | +| `eq` | Stack | 2 | 1 | Checks if `stack[0] == stack[1]`. Pushes `1` to the stack if equal, `0` otherwise | +| `not` | Stack | 1 | 1 | Inverses `stack[0]` | +| `gt` | Stack | 2 | 1 | Checks if `stack[0] > stack[1]`. Pushes `1` to the stack if greater, `0` otherwise | +| `lt` | Stack | 2 | 1 | Checks if `stack[0] < stack[1]`. Pushes `1` to the stack if smaller, `0` otherwise | +| `add` | Stack | 2 | 1 | Calculates `stack[0] + stack[1]`, and pushes the result to the stack | +| `sub` | Stack | 2 | 1 | Calculates `stack[0] - stack[1]`, and pushes the result to the stack | +| `mult` | Stack | 2 | 1 | Calculates `stack[0] * stack[1]`, and pushes the result to the stack | +| `div` | Stack | 2 | 1 | Calculates `stack[0] / stack[1]`, and pushes the result to the stack | +| `mod` | Stack | 2 | 1 | Calculates `stack[0] % stack[1]`, and pushes the result to the stack | +| `read` | Stack | 0 | 1 | Reads a character from the user, and pushes its char value to the stack | +| `print` | Stack | 1 | 0 | Prints `stack[0]` as a character | +| `jmp` | Program Flow, Stack | 1 | 0 | Jumps to the line number specified by `stack[0]` | +| `jmpc` | Program Flow, Stack | 2 | 0 | Jumps to the line number specified by `stack[0]`, if `stack[1]` is not `0`. | + +Please note that `stack[0]` refers to the topmost stack value, and `stack[i]` refers to the i-th stack value. + +## Source code + +The syntax of the source code is strongly aligned with the Instructions table above. However, there's a bit of *syntactical sugar* to make programming in this language enjoyable. Take a look into the `examples` directory of this repository to get a basic idea of it. + +### Comments + +Every line starting with `#`, `//` or `;` is ignored by the compiler. + +### Labels + +You can define labels like this: + +``` +// This program reads a character from the user, and subtracts 1 from it, until it is zero. +read + +// Let's loop here +LOOP: +push 1 +sub +push 0 +lt +push LOOP +jmpc + +// We're done! +``` + +As you can see, labels can be defined with a `:` after the label name, and it can be used as a value for `push`. At compile time, it is replaced with the corresponding line number. + +## Implementations + +There is a reference implementation for the compiler, `mexico`, and a reference implementation for the interpreter, `mexigo`. Both can be found in this repository. + +### Compiler "mexico" + +Simply run `go get github.com/maride/mexico/mexico` to get the compiler. + +The mexico compiler takes three arguments: + +- `-input` to specify the source code file +- `-output` to specify the output path for the zonefile +- `-baseDomain`, the base domain to compile the source code for. This should be the domain you are planning to host the source code on. + +For example. to compile the `Fibonacci.mxc` example for the domain `fibonacci.mxc.maride.cc`, you could use this command: + +`./mexico --input ../examples/Fibonacci.mxc --output /srv/zones/fibonacci.mxc.maride.cc --baseDomain fibonacci.mxc.maride.cc` + +If no problems occurred and the compiler didn't run into an issue, nothing is printed. + +### Interpreter "mexigo" + +Simply run `go get github.com/maride/mexico/mexigo` to get the interpreter. + +The mexigo interpreter takes only one argument - the domain to execute: + +`./mexigo fibonacci.mxc.maride.cc` + +This will give you an output similar to this: + +``` +> $ ./mexigo fibonacci.mxc.maride.cc +mexigo - the reference interpreter for the mexico esolang! +See github.com/maride/mexico for further information. + +2019/12/08 17:22:27 Resolving fibonacci.mxc.maride.cc for MX records +2019/12/08 17:22:27 Found 24 code lines, interpreting them... +'\x02' (2) +'\x03' (3) +'\x05' (5) +'\b' (8) +'\r' (13) +'\x15' (21) +'"' (34) +'7' (55) +'Y' (89) +'\u0090' (144) +'é' (233) +'Ź' (377) +'ɢ' (610) +'ϛ' (987) +'ؽ' (1597) +2019/12/08 17:22:27 Stack is currently 0 entries big +2019/12/08 17:22:27 Tape is currently 2 cells big +Cell 0: 987 (ϛ) +Cell 1: 1597 (ؽ) +2019/12/08 17:22:27 Found no commands after line 24. Stopping. +``` + +## Examples + +You can find examples in the `examples` directory of this repository. + +I currently host the `Fibonacci.mxc` on `fibonacci.mxc.maride.cc`, means you can run it with `mexigo fibonacci.mxc.maride.cc`! + +I challenge you to write more examples. ;) + +## License + +I chose to release this project under the [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) license. diff --git a/examples/Fibonacci.mxc b/examples/Fibonacci.mxc new file mode 100644 index 0000000..e8c3a06 --- /dev/null +++ b/examples/Fibonacci.mxc @@ -0,0 +1,45 @@ +// Fibonacci program written in mexico - calculating all results below 1337 + +// Set up tape +push 1 +pop +right +push 1 +pop +left + +// Fibonnacci loop +MAINLOOP: + // Calculate i + j + pusht + right + pusht + add + + // Print out result + dup + print + + // Overwrite i with j + pusht + left + pop + + // Write result of calculation to j, and duplicate it for further usage + dup + right + pop + + // Move head for a clean loop + left + + // Check if we should already stop + push 1337 + lt + not + push MAINLOOP + jmpc +// MAINLOOP END + +// Calculated all fibonacci numbers below 1337 :) yay! + diff --git a/examples/HelloWorld.mxc b/examples/HelloWorld.mxc new file mode 100644 index 0000000..b613d27 --- /dev/null +++ b/examples/HelloWorld.mxc @@ -0,0 +1,26 @@ +// A simple Hello World program, only working with the stack + +// Push "Hello World\0" in reverse order +push 0 +push 100 +push 108 +push 114 +push 111 +push 87 +push 32 +push 111 +push 108 +push 108 +push 101 +push 72 + +PRINTLOOP: + // Print until encountering null byte + dup + print + push 0 + eq + not + push PRINTLOOP + jmpc +// Done \ No newline at end of file diff --git a/mexico/compiler/codeline.go b/mexico/compiler/codeline.go new file mode 100644 index 0000000..6bdd736 --- /dev/null +++ b/mexico/compiler/codeline.go @@ -0,0 +1,6 @@ +package compiler + +type Codeline struct { + Linenumber int + Code string +} diff --git a/mexico/compiler/compiler.go b/mexico/compiler/compiler.go new file mode 100644 index 0000000..a4e761e --- /dev/null +++ b/mexico/compiler/compiler.go @@ -0,0 +1,179 @@ +package compiler + +import ( + "fmt" + "github.com/pkg/errors" + "strconv" + "strings" +) + +var ( + // List of commands which can be translated into FQDNs in one step. + passThroughCommands = []string{ + "left", + "right", + "pusht", + // push is missing here, because it is not a simple "pass-through" command; we need to process it further. + "pop", + "dup", + "del", + "eq", + "not", + "gt", + "lt", + "add", + "sub", + "mult", + "div", + "mod", + "read", + "print", + "jmp", + "jmpc", + } +) + +const ( + FakeFQDN = "mexico.invalid" +) + +// This is the compile function. As the name suggests, it compiles the source code handed over. +// For this task, it takes three steps: +// - Iterate over the source code and clean it (remove comments, remove empty lines, remove surrounding spaces) +// - Iterate over the source code, number each line and build up a label lookup table (mapping labels to line numbers) +// - Iterate over the source code and translate the instructions to valid MX records +func Compile(lines []string, domain string) ([]Codeline, error) { + numberedCode, labelLookupTable := numberLines(cleanCode(lines)) + return translateLines(numberedCode, labelLookupTable, domain) +} + +// Cleans the lines in the string array: remove comments, remove empty lines, remove spaces +func cleanCode(lines []string) []string { + var cleanLines []string + + // Iterate over all code lines, and clean them + for _, l := range lines { + // Remove surrounding spaces and tabs + l = strings.Trim(l, " \t") + + // Check if line is a comment + if strings.HasPrefix(l, "#") || strings.HasPrefix(l, ";") || strings.HasPrefix(l, "//") { + // It is a comment line, ignore + continue + } + + // Check if line is empty + if len(l) == 0 { + // Empty line, ignore. + continue + } + + // If we reach this point, the line is ready to be added to the list of cleaned lines + cleanLines = append(cleanLines, l) + } + + // Return cleaned lines + return cleanLines +} + +// Adds line numbers to the code lines, and builds up a label lookup table, mapping label to line numbers +func numberLines(lines []string) ([]Codeline, map[string]int) { + var code []Codeline + var labelLookup = make(map[string]int) + linenumber := 0 + + // Iterate over all (string) lines and convert them to codelines + for _, l := range lines { + // Check if line is a label - defined by ':' at the end + if l[len(l)-1] == ':' { + // It's a label. Write it into the lookup table + name := l[:len(l)-1] + labelLookup[name] = linenumber + + // And skip further execution, to avoid raising the line number or appending this line to the code array + continue + } + + // Append codeline to the array + code = append(code, Codeline{ + Linenumber: linenumber, + Code: l, + }) + + // Raise line number + linenumber++ + } + + // Returns the code lines and the label lookup table + return code, labelLookup +} + +// Translates the code into FQDNs to be further used for MX records, resolving labels to their line numbers +func translateLines(code []Codeline, labelLookup map[string]int, domain string) ([]Codeline, error) { + // Iterate over all commands + for i := 0; i < len(code); i++ { + // Check if command is part of "pass-through" commands. + found := false + for _, c := range passThroughCommands { + if code[i].Code == c { + // It is! We can simply translate it to a FQDN without further processing required. + code[i].Code = fmt.Sprintf("%s.%s.", c, FakeFQDN) + found = true + break + } + } + + // Check if command was a pass-through command + if found { + // It was, go on to the next command + continue + } + + // Check if it is the push command + if code[i].Code[:4] == "push" { + // It is. Either we have a constant here, or a label name - let's check. + maybeLabelMaybeConstant := code[i].Code[5:] + maybeLinenumberMaybeNot := getNumberForLabel(maybeLabelMaybeConstant, labelLookup) + + // if maybeLinenumberMaybeNot is not -1, there is a label with this name :) + if maybeLinenumberMaybeNot > -1 { + // Found linenumber for that label - write it into code + code[i].Code = fmt.Sprintf("push-%d.%s.", maybeLinenumberMaybeNot, FakeFQDN) + continue + } + + // If we are at this point, we didn't find a label with that name. Try to parse it as a number + maybeValueMaybeNot, atoiErr := strconv.Atoi(maybeLabelMaybeConstant) + + // Throw error if parsing didn't work + if atoiErr != nil { + errorString := fmt.Sprintf("Not a label or a integer constant: '%d'. %s", maybeLinenumberMaybeNot, atoiErr.Error()) + return nil, errors.New(errorString) + } + + // If we are at this point, we were able to parse the mystery value as an integer. Yay! + code[i].Code = fmt.Sprintf("push-%d.%s.", maybeValueMaybeNot, FakeFQDN) + continue + } + + // uh, if we reach this point, it's not a valid command - return + return nil, errors.New(fmt.Sprintf("Not a valid command: %s", code[i].Code)) + } + + // And return our constructed source code + return code, nil +} + +// Returns the line number for the given label, or -1 if no such label was found +func getNumberForLabel(label string, labelLookup map[string]int) int { + // iterate over all labels + for l, number := range labelLookup { + if l == label { + // Found, return number + return number + } + } + + // None found, return -1 + return -1 +} \ No newline at end of file diff --git a/mexico/io.go b/mexico/io.go new file mode 100644 index 0000000..1032e7d --- /dev/null +++ b/mexico/io.go @@ -0,0 +1,62 @@ +package main + +import ( + "flag" + "fmt" + "github.com/maride/mexico/mexico/compiler" + "io/ioutil" + "strings" + "time" +) + +var ( + inputFilePath *string + outputFilePath *string + baseDomain *string +) + +// Registers flags required for input and output +func registerIOFlags() { + inputFilePath = flag.String("input", "", "Name of the source code file to read") + outputFilePath = flag.String("output", "", "Name of the zonefile to write") + baseDomain = flag.String("baseDomain", "mexico.invalid", "The base domain to write the zonefile for") +} + +// Reads the input file, splits it at newlines, and returns it as a string array +func readFile() ([]string, error) { + // Read file + fileBytes, readErr := ioutil.ReadFile(*inputFilePath) + if readErr != nil { + // Error reading the file, pass through + return nil, readErr + } + + // And split along newlines + return strings.Split(string(fileBytes), "\n"), nil +} + +// Writes the code lines into the format of a Zonefile +func writeZone(code []compiler.Codeline) error { + var zone strings.Builder + domain := *baseDomain + + // Check if we need to append a dot to the end of the domain name + if (*baseDomain)[len(*baseDomain)-1] != '.' { + // No, append a dot + domain = *baseDomain + "." + } + + // Generate values for further usage + serial := time.Now().Format("2006010215") // YYYYMMDDHH + + // Write SOA record + zone.WriteString(fmt.Sprintf("%s\tIN SOA\t%s mexico.%s (%s 1h 1h 1h 1h)\n", domain, domain, domain, serial)) + + // Write every other record + for _, c := range code { + zone.WriteString(fmt.Sprintf("%s\tIN MX\t%d %s\n", domain, c.Linenumber, c.Code)) + } + + // And write built string to file + return ioutil.WriteFile(*outputFilePath, []byte(zone.String()), 0644) +} \ No newline at end of file diff --git a/mexico/main.go b/mexico/main.go new file mode 100644 index 0000000..2292fa2 --- /dev/null +++ b/mexico/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "flag" + "github.com/maride/mexico/mexico/compiler" + "log" +) + +func main() { + // Register flags + registerIOFlags() + flag.Parse() + + // Read file + fileContent, readErr := readFile() + handleErr(readErr) + + // Parse and compile lines + code, compileErr := compiler.Compile(fileContent, *baseDomain) + handleErr(compileErr) + + // Write Zonefile with given code + writeErr := writeZone(code) + handleErr(writeErr) +} + + +// Checks if an error is present, and raises it. +func handleErr(err error) { + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/mexigo/interpreter/codeline.go b/mexigo/interpreter/codeline.go new file mode 100644 index 0000000..2c7530b --- /dev/null +++ b/mexigo/interpreter/codeline.go @@ -0,0 +1,6 @@ +package interpreter + +type Codeline struct { + Linenumber int + Code string +} diff --git a/mexigo/interpreter/debug.go b/mexigo/interpreter/debug.go new file mode 100644 index 0000000..7790f91 --- /dev/null +++ b/mexigo/interpreter/debug.go @@ -0,0 +1,20 @@ +package interpreter + +import ( + "fmt" + "log" +) + +func (t *Tape) DebugPrintTape() { + log.Printf("Tape is currently %d cells big", len(t.cells)) + for i, v := range t.cells { + fmt.Printf("Cell %d: %d (%c)\n", i, v, v) + } +} + +func (s * Stack) DebugPrintStack() { + log.Printf("Stack is currently %d entries big", len(s.values)) + for i, v := range s.values { + fmt.Printf("Stack row %d: %d (%c)\n", i, v, v) + } +} diff --git a/mexigo/interpreter/interpreter.go b/mexigo/interpreter/interpreter.go new file mode 100644 index 0000000..cb2a20e --- /dev/null +++ b/mexigo/interpreter/interpreter.go @@ -0,0 +1,89 @@ +package interpreter + +import ( + "fmt" + "github.com/pkg/errors" +) + +type Interpreter struct { + machine Machine + program []Codeline + programCounter int + programPointer int +} + +// Feeds a new mexico machine with given code. +func Run(commands []Codeline) error { + var i Interpreter + + // Set the given code as commands for the interpreter + setCmdErr := i.SetCommands(commands) + if setCmdErr != nil { + return setCmdErr + } + + // Let's run this program :) + return i.Run() +} + +// Sets the given array as new program for the interpreter +func (i *Interpreter) SetCommands(commands []Codeline) error { + i.program = commands + i.programCounter = 0 + return i.GoToNextCommand() +} + +// Searches for the next command, starting from the current value of the programCounter. +// This may sound odd, because in most other architectures, this is just programCounter++, and there would be no need +// for a function like this. However, mexico has a BASIC-style program line numbering, means we need to search for the +// next line number containing code, because there may be one or more empty lines between the current and the next line. +// This is exactly what GoToNextCommand() does. +// If there is no next command, most likely because we reached the end of the program, an error is thrown. +func (i *Interpreter) GoToNextCommand() error { + // Iterate over all lines to find the first which has a greater line number than the current one + for index, line := range i.program { + if line.Linenumber >= i.programCounter { + // Found, set and return + i.programCounter = line.Linenumber + i.programPointer = index + return nil + } + } + + // No next command found. Throw error. + return errors.New(fmt.Sprintf("Found no commands after line %d. Stopping.", i.programCounter)) +} + +// Runs the commands, unless an error is encountered, then it doesn't run the commands. +func (i *Interpreter) Run() error { + defer i.machine.Tape.DebugPrintTape() + defer i.machine.Stack.DebugPrintStack() + + for { + // Get current command + cmd := i.program[i.programPointer] + + // Run command in the machine + jumpLine, doJump, runErr := i.machine.RunCommand(cmd.Code) + if runErr != nil { + // Encountered an error during runtime, stop execution + return runErr + } + + // Check if we should jump anywhere else than to the next code line + if doJump { + // Yes, do it then. + i.programCounter = jumpLine + } else { + // We are not asked to jump anywhere, move on to the next code line then. + i.programCounter++ + } + + // Skip empty lines if there are any. + nextCmdErr := i.GoToNextCommand() + if nextCmdErr != nil { + // Encountered error finding the next command, throw it + return nextCmdErr + } + } +} diff --git a/mexigo/interpreter/machine.go b/mexigo/interpreter/machine.go new file mode 100644 index 0000000..2586adc --- /dev/null +++ b/mexigo/interpreter/machine.go @@ -0,0 +1,160 @@ +package interpreter + +import ( + "fmt" + "github.com/pkg/errors" + "os" + "strconv" + "strings" +) + +type Machine struct { + Stack Stack + Tape Tape +} + +// Runs the given command. +// Returns the next line (comparable to the 'Program Counter') to be executed, but just if doJump is true. +// May also return an error. It's advised to stop the execution of further commands if this command throws an error. +func (m *Machine) RunCommand(cmd string) (jumpLine int, doJump bool, execErr error) { + // Let's check which command we are told to run. + if cmd == "left" { + // Moves the tape head one cell to the left + m.Tape.MoveLeft() + } else if cmd == "right" { + // Moves the tape head one cell to the right + m.Tape.MoveRight() + } else if cmd == "pusht" { + // Reads the current cell value and pushes it on top of the stack + m.Stack.Push(m.Tape.Get()) + } else if strings.HasPrefix(cmd, "push ") { + // Pushes the value n to the stack + + // Cut "push " away and trim + strVal := strings.Trim(cmd[5:], " ") + + // Convert to integer + intVal, atoiErr := strconv.Atoi(strVal) + if atoiErr != nil { + // Conversion failed. + execErr = errors.New(fmt.Sprintf("Tried to push non-integer value '%s' to the stack. %s", strVal, atoiErr.Error())) + return + } else { + // Conversion successful, push constant + m.Stack.Push(intVal) + } + } else if cmd == "pop" { + // Pops top stack value to the current cell + m.Tape.Set(m.Stack.Pop()) + } else if cmd == "dup" { + // Duplicates the topmost stack value + val := m.Stack.Pop() + m.Stack.Push(val) + m.Stack.Push(val) + } else if cmd == "del" { + // Deletes the topmost stack value, ignoring its value + m.Stack.Pop() + } else if cmd == "eq" { + // Checks if stack[0] == stack[1]. Pushes 1 to the stack if equal, 0 otherwise + stack0 := m.Stack.Pop() + stack1 := m.Stack.Pop() + + if stack0 == stack1 { + m.Stack.Push(1) + } else { + m.Stack.Push(0) + } + } else if cmd == "not" { + // Inverses stack[0] + stack0 := m.Stack.Pop() + + if stack0 == 0 { + m.Stack.Push(1) + } else if stack0 == 1 { + m.Stack.Push(0) + } else { + // Not a binary number, not going to inverse it. + execErr = errors.New(fmt.Sprintf("Tried to inverse non-binary integer value '%d'", stack0)) + return + } + } else if cmd == "gt" { + // Checks if stack[0] > stack[1]. Pushes 1 to the stack if greater, 0 otherwise + stack0 := m.Stack.Pop() + stack1 := m.Stack.Pop() + + if stack0 > stack1 { + m.Stack.Push(1) + } else { + m.Stack.Push(0) + } + } else if cmd == "lt" { + // Checks if stack[0] < stack[1]. Pushes 1 to the stack if greater, 0 otherwise + stack0 := m.Stack.Pop() + stack1 := m.Stack.Pop() + + if stack0 < stack1 { + m.Stack.Push(1) + } else { + m.Stack.Push(0) + } + } else if cmd == "add" { + // Calculates stack[0] + stack[1], and pushes the result to the stack + m.Stack.Push(m.Stack.Pop() + m.Stack.Pop()) + } else if cmd == "sub" { + // Calculates stack[0] - stack[1], and pushes the result to the stack + m.Stack.Push(m.Stack.Pop() - m.Stack.Pop()) + } else if cmd == "mult" { + // Calculates stack[0] * stack[1], and pushes the result to the stack + m.Stack.Push(m.Stack.Pop() * m.Stack.Pop()) + } else if cmd == "div" { + // Calculates stack[0] / stack[1], and pushes the result to the stack + m.Stack.Push(m.Stack.Pop() / m.Stack.Pop()) + } else if cmd == "mod" { + // Calculates stack[0] % stack[1], and pushes the result to the stack + m.Stack.Push(m.Stack.Pop() % m.Stack.Pop()) + } else if cmd == "read" { + // Reads a character from the user, and pushes its char value to the stack + var readChar []byte + + // Read character(s) + numChars, readErr := os.Stdin.Read(readChar) + if readErr != nil { + // Failed to read from stdin. + execErr = errors.New(fmt.Sprintf("Failed to read character from stdin: %s", readErr.Error())) + return + } + + if numChars < 1 || readChar == nil { + // Didn't even read a single character. + execErr = errors.New("Failed to read character from stdin.") + return + } + + // Push character to stack + m.Stack.Push(int(readChar[0])) + + // Check if user typed more than asked for + if numChars > 1 { + // Ugh, what a spammer + execErr = errors.New("Read more than one character - ignoring all but the first character.") + return + } + } else if cmd == "print" { + // Prints stack[0] as a character + val := m.Stack.Pop() + fmt.Printf("%q (%d)\n", rune(val), val) + } else if cmd == "jmp" { + // Jumps to the line number specified by stack[0] + jumpLine = m.Stack.Pop() + doJump = true + } else if cmd == "jmpc" { + // Jumps to the line number specified by stack[0], if stack[1] is not 0. + jumpLine = m.Stack.Pop() + doJump = m.Stack.Pop() != 0 + } else { + // ... no such command. + execErr = errors.New(fmt.Sprintf("Command not found: %s", cmd)) + } + + return +} diff --git a/mexigo/interpreter/stack.go b/mexigo/interpreter/stack.go new file mode 100644 index 0000000..78500e1 --- /dev/null +++ b/mexigo/interpreter/stack.go @@ -0,0 +1,27 @@ +package interpreter + +import "log" + +type Stack struct { + values []int +} + +// Pushes the given value to the stack +func (s *Stack) Push(val int) { + s.values = append(s.values, val) +} + +// Pops the top element from the stack and return its value +func (s *Stack) Pop() int { + // Check if the stack contains at least one element + if len(s.values) > 0 { + // There's at least one element, pop it: get value and delete element + val := s.values[len(s.values)-1] + s.values = s.values[:len(s.values)-1] + return val + } + + // Stack is empty, but we should pop... Damn. + log.Panic("Tried to pop value from empty stack.") + return 0 +} diff --git a/mexigo/interpreter/tape.go b/mexigo/interpreter/tape.go new file mode 100644 index 0000000..3df2279 --- /dev/null +++ b/mexigo/interpreter/tape.go @@ -0,0 +1,51 @@ +package interpreter + +type Tape struct { + head uint + cells []int +} + +// Sets the head to point to the specified position +func (t *Tape) SetHead(pos uint) { + t.GrowUpTo(pos) + t.head = pos +} + +// Resizes the tape to the given size. +// If the cell array is already bigger than the specified size, nothing happens. +// If the cell array is smaller than the specified size, it's resized to the given size and filled with the value 0. +func (t *Tape) GrowUpTo(size uint) { + if uint(len(t.cells)) > size { + // Tape is already bigger. Do nothing. + return + } + + // Append as many zeroes as required. + for uint(len(t.cells)) <= size { + t.cells = append(t.cells, 0) + } +} + +// Moves the tape head left +func (t *Tape) MoveLeft() { + if t.head > 0 { + t.SetHead(t.head - 1) + } +} + +// Moves the tape head left +func (t *Tape) MoveRight() { + t.SetHead(t.head + 1) +} + +// Returns the current cell value +func (t *Tape) Get() int { + t.GrowUpTo(t.head) + return t.cells[t.head] +} + +// Sets the current cell to the new value +func (t *Tape) Set(newVal int) { + t.GrowUpTo(t.head) + t.cells[t.head] = newVal +} diff --git a/mexigo/main.go b/mexigo/main.go new file mode 100644 index 0000000..e551f4a --- /dev/null +++ b/mexigo/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "flag" + "github.com/maride/mexico/mexigo/interpreter" + "log" +) + +func main() { + // Important things first + printBanner() + + // Get desired domain off arguments + flag.Parse() + domain := flag.Arg(0) + if domain == "" { + // No domain entered. + log.Println("Please specify a domain to receive code from as first argument, like this: ./mexigo ") + return + } + + // Get program code from that domain + log.Printf("Resolving %s for MX records", domain) + code := LookupMX(domain) + if len(code) == 0 { + // Failed to look up mexico code on that domain. Log and exit. + log.Printf("No code found on domain '%s'. Exiting.", domain) + return + } + + // Inform user about successful resolving + log.Printf("Found %d code lines, interpreting them...", len(code)) + + // Set up interpreter + runErr := interpreter.Run(code) + if runErr != nil { + // Encountered error while executing code. Log and exit. + log.Println(runErr.Error()) + return + } +} + +// Prints a small banner :) +func printBanner() { + println("mexigo - the reference interpreter for the mexico esolang!") + println("See github.com/maride/mexico for further information.") + println() +} diff --git a/mexigo/resolver.go b/mexigo/resolver.go new file mode 100644 index 0000000..0279248 --- /dev/null +++ b/mexigo/resolver.go @@ -0,0 +1,71 @@ +package main + +import ( + "github.com/maride/mexico/mexigo/interpreter" + "log" + "math" + "net" + "strings" +) + +const ( + // The fake base domain which classifies a domain name as a mexico command, rather than a "normal" domain name + MexicoFakeDomain = "mexico.invalid." +) + +// This is a wrapper function for net.LookupMX(), filtering for mexico records, and sorting the remaining by linenum +func LookupMX(basedomain string) []interpreter.Codeline { + // Do the basic lookup + rawMX, lookupErr := net.LookupMX(basedomain) + if lookupErr != nil { + // Encountered error while looking up basedomain - log and return + log.Printf("Failed to resolve '%s': %s", basedomain, lookupErr.Error()) + return nil + } + + // Filter results for the (fake) domain "mexico.invalid." + var filteredMX []*net.MX + + // Iterate over all returned MX records + for _, raw := range rawMX { + // Check if it's a mexico MX record + if strings.HasSuffix(raw.Host, MexicoFakeDomain) { + // it is, add to filtered array + filteredMX = append(filteredMX, raw) + } + } + + // Sort filtered results, based on the priority - or line number, in the words of this esolang :) + var records []interpreter.Codeline + var smallestPriority uint16 = math.MaxInt16 + smallestPriorityIndex := 0 + + // Iterate over filteredMX and delete the record with the smallest priority until we don't have any more filteredMX + for len(filteredMX) > 0 { + // Iterate over the filteredMX to find the one with the smallest priority + for i, f := range filteredMX { + if f.Pref < smallestPriority { + // Found entry with smaller index than the current one + smallestPriority = f.Pref + smallestPriorityIndex = i + } + } + + // Remove mexico fake domain suffix + command := strings.TrimSuffix(filteredMX[smallestPriorityIndex].Host, "." + MexicoFakeDomain) + + // Replace '-' with space. + // This reserves the process done by the compiler to transform this command + arg into a FQDN + command = strings.Replace(command, "-", " ", 1) + + // Add hostname of "smallest" record to the records array, and delete it from filteredMX + records = append(records, interpreter.Codeline{ + Linenumber: int(filteredMX[smallestPriorityIndex].Pref), + Code: command, + }) + filteredMX = append(filteredMX[:smallestPriorityIndex], filteredMX[smallestPriorityIndex + 1:]...) + } + + // Return filtered and sorted records + return records +}