Lassen Sie uns eine winzige Programmiersprache erstellen

Avatar of Md Shuvo
Md Shuvo am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Bis jetzt sind Sie wahrscheinlich mit einer oder mehreren Programmiersprachen vertraut. Aber haben Sie sich jemals gefragt, wie Sie Ihre eigene Programmiersprache erstellen könnten? Und damit meine ich

Eine Programmiersprache ist jede Regelmenge, die Zeichenketten in verschiedene Arten von Maschinencode-Ausgaben umwandelt.

Kurz gesagt, eine Programmiersprache ist nur eine Reihe vordefinierter Regeln. Und um sie nützlich zu machen, benötigen Sie etwas, das diese Regeln versteht. Und diese Dinge sind Compiler, Interpreter usw. Wir können also einfach einige Regeln definieren und dann, um sie zum Laufen zu bringen, können wir jede bestehende Programmiersprache verwenden, um ein Programm zu erstellen, das diese Regeln versteht, was unser Interpreter sein wird.

Compiler

Ein Compiler wandelt Code in Maschinencode um, den der Prozessor ausführen kann (z. B. C++-Compiler).

Interpreter

Ein Interpreter durchläuft das Programm Zeile für Zeile und führt jeden Befehl aus.

Möchten Sie es ausprobieren? Lassen Sie uns gemeinsam eine super einfache Programmiersprache erstellen, die magentafarbene Ausgaben in der Konsole erzeugt. Wir werden sie Magenta nennen.

Screenshot of terminal output in color magenta.
Unsere einfache Programmiersprache erstellt eine Variable namens `codes`, die Text enthält, der in der Konsole ausgegeben wird ... natürlich in Magenta.

Einrichtung unserer Programmiersprache

Ich werde Node.js verwenden, aber Sie können jede Sprache verwenden, um dem zu folgen, das Konzept wird dasselbe bleiben. Lassen Sie mich beginnen, indem ich eine `index.js`-Datei erstelle und die Dinge einrichte.

class Magenta {
  constructor(codes) {
    this.codes = codes
  }
  run() {
    console.log(this.codes)
  }
}

// For now, we are storing codes in a string variable called `codes`
// Later, we will read codes from a file
const codes = 
`print "hello world"
print "hello again"`
const magenta = new Magenta(codes)
magenta.run()

Was wir hier tun, ist die Deklaration einer Klasse namens `Magenta`. Diese Klasse definiert und initialisiert ein Objekt, das für die Protokollierung von Text in der Konsole mit beliebigem Text verantwortlich ist, den wir über eine `codes`-Variable bereitstellen. Und vorerst haben wir diese `codes`-Variable direkt in der Datei mit ein paar "Hallo"-Nachrichten definiert.

Screenshot of terminal output.
Wenn wir diesen Code ausführen würden, würden wir den in `codes` gespeicherten Text in der Konsole protokolliert sehen.

OK, jetzt müssen wir einen sogenannten Lexer erstellen.

Was ist ein Lexer?

Okay, sprechen wir kurz über die englische Sprache. Nehmen Sie die folgende Phrase

Wie geht es Ihnen?

Hier ist "How" ein Adverb, "are" ein Verb und "you" ein Pronomen. Wir haben auch ein Fragezeichen ("?") am Ende. Wir können jeden Satz oder jede Phrase wie diese in viele grammatikalische Komponenten in JavaScript zerlegen. Eine andere Möglichkeit, diese Teile zu unterscheiden, besteht darin, sie in kleine Token zu zerlegen. Das Programm, das den Text in Token zerlegt, ist unser Lexer.

Diagram showing command going through a lexer.

Da unsere Sprache sehr klein ist, hat sie nur zwei Arten von Token mit jeweils einem Wert

  1. Schlüsselwort
  2. Zeichenkette

Wir hätten eine reguläre Expression verwenden können, um Tokens aus dem `codes`-String zu extrahieren, aber die Leistung wäre sehr langsam. Ein besserer Ansatz ist es, jeden Charakter des `code`-Strings zu durchlaufen und Tokens zu erfassen. Erstellen wir also eine `tokenize`-Methode in unserer `Magenta`-Klasse – die unser Lexer sein wird.

Vollständiger Code
class Magenta {
  constructor(codes) {
    this.codes = codes
  }
  tokenize() {
    const length = this.codes.length
    // pos keeps track of current position/index
    let pos = 0
    let tokens = []
    const BUILT_IN_KEYWORDS = ["print"]
    // allowed characters for variable/keyword
    const varChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'
    while (pos < length) {
      let currentChar = this.codes[pos]
      // if current char is space or newline,  continue
      if (currentChar === " " || currentChar === "\n") {
        pos++
        continue
      } else if (currentChar === '"') {
        // if current char is " then we have a string
        let res = ""
        pos++
        // while next char is not " or \n and we are not at the end of the code
        while (this.codes[pos] !== '"' && this.codes[pos] !== '\n' && pos < length) {
          // adding the char to the string
          res += this.codes[pos]
          pos++
        }
        // if the loop ended because of the end of the code and we didn't find the closing "
        if (this.codes[pos] !== '"') {
          return {
            error: `Unterminated string`
          }
        }
        pos++
        // adding the string to the tokens
        tokens.push({
          type: "string",
          value: res
        })
      } else if (varChars.includes(currentChar)) {
        let res = currentChar
        pos++
        // while the next char is a valid variable/keyword charater
        while (varChars.includes(this.codes[pos]) && pos < length) {
          // adding the char to the string
          res += this.codes[pos]
          pos++
        }
        // if the keyword is not a built in keyword
        if (!BUILT_IN_KEYWORDS.includes(res)) {
          return {
            error: `Unexpected token ${res}`
          }
        }
        // adding the keyword to the tokens
        tokens.push({
          type: "keyword",
          value: res
        })
      } else { // we have a invalid character in our code
        return {
          error: `Unexpected character ${this.codes[pos]}`
        }
      }
    }
    // returning the tokens
    return {
      error: false,
      tokens
    }
  }
  run() {
    const {
      tokens,
      error
    } = this.tokenize()
    if (error) {
      console.log(error)
      return
    }
    console.log(tokens)
  }
}

Wenn wir dies in einem Terminal mit `node index.js` ausführen, sollten wir eine Liste von Token in der Konsole sehen.

Screenshot of code.
Großartige Arbeit!

Definition von Regeln und Syntaxen

Wir wollen sehen, ob die Reihenfolge unserer Codes einer Regel oder Syntax entspricht. Aber zuerst müssen wir definieren, was diese Regeln und Syntaxen sind. Da unsere Sprache so klein ist, hat sie nur eine einfache Syntax: ein `print`-Schlüsselwort, gefolgt von einer Zeichenkette.

keyword:print string

Erstellen wir also eine `parse`-Methode, die unsere Token durchläuft und prüft, ob eine gültige Syntax gebildet wurde. Wenn ja, werden die notwendigen Maßnahmen ergriffen.

class Magenta {
  constructor(codes) {
    this.codes = codes
  }
  tokenize(){
    /* previous codes for tokenizer */
  }
  parse(tokens){
    const len = tokens.length
    let pos = 0
    while(pos < len) {
      const token = tokens[pos]
      // if token is a print keyword
      if(token.type === "keyword" && token.value === "print") {
        // if the next token doesn't exist
        if(!tokens[pos + 1]) {
          return console.log("Unexpected end of line, expected string")
        }
        // check if the next token is a string
        let isString = tokens[pos + 1].type === "string"
        // if the next token is not a string
        if(!isString) {
          return console.log(`Unexpected token ${tokens[pos + 1].type}, expected string`)
        }
        // if we reach this point, we have valid syntax
        // so we can print the string
        console.log('\x1b[35m%s\x1b[0m', tokens[pos + 1].value)
        // we add 2 because we also check the token after print keyword
        pos += 2
      } else{ // if we didn't match any rules
        return console.log(`Unexpected token ${token.type}`)
      }
    }
  }
  run(){
    const {tokens, error} = this.tokenize()
    if(error){
      console.log(error)
      return
    }
    this.parse(tokens)
  }
}

Und schauen Sie mal – wir haben bereits eine funktionierende Sprache!

Screenshot of terminal output.

Okay, aber Codes in einer Zeichenkettenvariable zu haben, ist nicht so spaßig. Also legen wir unsere Magenta-Codes in eine Datei namens `code.m`. So können wir unsere Magenta-Codes von der Compiler-Logik trennen. Wir verwenden `.m` als Dateierweiterung, um anzuzeigen, dass diese Datei Code für unsere Sprache enthält.

Lassen Sie uns den Code aus dieser Datei lesen

// importing file system module
const fs = require('fs')
//importing path module for convenient path joining
const path = require('path')
class Magenta{
  constructor(codes){
    this.codes = codes
  }
  tokenize(){
    /* previous codes for tokenizer */
 }
  parse(tokens){
    /* previous codes for parse method */
 }
  run(){
    /* previous codes for run method */
  }
}

// Reading code.m file
// Some text editors use \r\n for new line instead of \n, so we are removing \r
const codes = fs.readFileSync(path.join(__dirname, 'code.m'), 'utf8').toString().replace(/\r/g, &quot;&quot;)
const magenta = new Magenta(codes)
magenta.run()

Gehen Sie eine Programmiersprache erstellen!

Und damit haben wir erfolgreich eine winzige Programmiersprache von Grund auf neu erstellt. Sehen Sie, eine Programmiersprache kann so einfach sein wie etwas, das eine bestimmte Aufgabe erfüllt. Sicher, es ist unwahrscheinlich, dass eine Sprache wie Magenta jemals nützlich genug sein wird, um Teil eines beliebten Frameworks zu sein, aber jetzt wissen Sie, was dazu gehört, eine zu erstellen.

Den Himmel gesetzt sind wirklich die Grenzen. Wenn Sie tiefer eintauchen möchten, folgen Sie diesem Video, das ich erstellt habe und in dem ich ein fortgeschritteneres Beispiel durchgehe. In diesem Video habe ich auch gezeigt, wie Sie Ihrer Sprache Variablen hinzufügen können.