mirror of
				https://github.com/maride/mexico.git
				synced 2025-10-10 18:56:49 +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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user