Init commit

This commit is contained in:
maride 2019-12-08 17:55:49 +01:00
commit 3c7376cca6
15 changed files with 977 additions and 0 deletions

154
README.md Normal file
View File

@ -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 <line 10>
someprogram.esolang.mil IN MX 20 <line 20>
someprogram.esolang.mil IN MX 30 <line 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.

45
examples/Fibonacci.mxc Normal file
View File

@ -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!

26
examples/HelloWorld.mxc Normal file
View File

@ -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

View File

@ -0,0 +1,6 @@
package compiler
type Codeline struct {
Linenumber int
Code string
}

179
mexico/compiler/compiler.go Normal file
View File

@ -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
}

62
mexico/io.go Normal file
View File

@ -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)
}

33
mexico/main.go Normal file
View File

@ -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())
}
}

View File

@ -0,0 +1,6 @@
package interpreter
type Codeline struct {
Linenumber int
Code string
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

48
mexigo/main.go Normal file
View File

@ -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 <domain>")
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()
}

71
mexigo/resolver.go Normal file
View File

@ -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
}