mirror of
https://github.com/maride/mexico.git
synced 2024-11-12 22:34:25 +00:00
Init commit
This commit is contained in:
commit
3c7376cca6
154
README.md
Normal file
154
README.md
Normal 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
45
examples/Fibonacci.mxc
Normal 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
26
examples/HelloWorld.mxc
Normal 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
|
6
mexico/compiler/codeline.go
Normal file
6
mexico/compiler/codeline.go
Normal file
@ -0,0 +1,6 @@
|
||||
package compiler
|
||||
|
||||
type Codeline struct {
|
||||
Linenumber int
|
||||
Code string
|
||||
}
|
179
mexico/compiler/compiler.go
Normal file
179
mexico/compiler/compiler.go
Normal 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
62
mexico/io.go
Normal 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
33
mexico/main.go
Normal 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())
|
||||
}
|
||||
}
|
6
mexigo/interpreter/codeline.go
Normal file
6
mexigo/interpreter/codeline.go
Normal file
@ -0,0 +1,6 @@
|
||||
package interpreter
|
||||
|
||||
type Codeline struct {
|
||||
Linenumber int
|
||||
Code string
|
||||
}
|
20
mexigo/interpreter/debug.go
Normal file
20
mexigo/interpreter/debug.go
Normal 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)
|
||||
}
|
||||
}
|
89
mexigo/interpreter/interpreter.go
Normal file
89
mexigo/interpreter/interpreter.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
160
mexigo/interpreter/machine.go
Normal file
160
mexigo/interpreter/machine.go
Normal 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
|
||||
}
|
27
mexigo/interpreter/stack.go
Normal file
27
mexigo/interpreter/stack.go
Normal 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
|
||||
}
|
51
mexigo/interpreter/tape.go
Normal file
51
mexigo/interpreter/tape.go
Normal 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
48
mexigo/main.go
Normal 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
71
mexigo/resolver.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user