Логотип Зефирнет

Давайте создадим крошечный язык программирования

Дата:

К настоящему времени вы, вероятно, знакомы с одним или несколькими языками программирования. Но задумывались ли вы когда-нибудь, как создать свой собственный язык программирования? И под этим я подразумеваю:

Язык программирования — это любой набор правил, которые преобразуют строки в различные виды вывода машинного кода.

Короче говоря, язык программирования — это просто набор предопределенных правил. И чтобы сделать их полезными, вам нужно что-то, что понимает эти правила. И эти вещи компиляторы, переводчики, и т. д. Таким образом, мы можем просто определить некоторые правила, затем, чтобы заставить его работать, мы можем использовать любой существующий язык программирования, чтобы создать программу, которая может понимать эти правила, которая будет нашим интерпретатором.

составитель

Компилятор преобразует коды в машинный код, который может выполнять процессор (например, компилятор C++).

переводчик

Интерпретатор просматривает программу строка за строкой и выполняет каждую команду.

Хотите попробовать? Давайте вместе создадим сверхпростой язык программирования, который выводит выходные данные пурпурного цвета в консоль. Мы назовем это фуксин.

Наш простой язык программирования создает переменную кодов, содержащую текст, который выводится на консоль… разумеется, пурпурным цветом.

Настраиваем наш язык программирования

Я собираюсь использовать Node.js, но вы можете использовать любой язык, концепция останется прежней. Позвольте мне начать с создания index.js файл и настроить вещи.

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

Здесь мы объявляем класс с именем Magenta. Этот класс определяет и инициирует объект, который отвечает за запись текста в консоль с любым текстом, который мы предоставляем через codes переменная. И на данный момент мы определили, что codes переменная прямо в файле с парой приветственных сообщений.

Скрин вывода терминала.
Если бы мы запустили этот код, мы бы получили текст, хранящийся в кодах, зарегистрированных в консоли.

Хорошо, теперь нам нужно создать то, что называется Lexer.

Что такое лексер?

Хорошо, давайте на секунду поговорим об английском языке. Возьмите следующую фразу:

Как поживаешь?

Здесь «как» — наречие, «есть» — глагол, а «ты» — местоимение. У нас также есть вопросительный знак («?») в конце. В JavaScript мы можем разделить любое предложение или фразу на множество грамматических компонентов. Другой способ, которым мы можем различать эти части, состоит в том, чтобы разделить их на маленькие токены. Программа, которая делит текст на токены, и есть наша лексер.

Диаграмма, показывающая, как команда проходит через лексер.

Так как наш язык очень маленький, он имеет только два типа токенов, каждый со значением:

  1. keyword
  2. string

Мы могли бы использовать регулярное выражение для извлечения маркеров из codes string, но производительность будет очень низкой. Лучший подход состоит в том, чтобы перебрать каждый символ code строка и захватить жетоны. Итак, давайте создадим tokenize метод в нашем Magenta class — который будет нашим лексером.

Полный код
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)) { arater
        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)
  }
}

Если мы запустим это в терминале с node index.js, мы должны увидеть список токенов, напечатанных в консоли.

Скриншот кода.
Большой материал!

Определение правил и синтаксисов

Мы хотим увидеть, соответствует ли порядок наших кодов какому-то правилу или синтаксису. Но сначала нам нужно определить, что это за правила и синтаксис. Поскольку наш язык такой крошечный, он имеет только один простой синтаксис, который представляет собой print ключевое слово, за которым следует строка.

keyword:print string

Итак, давайте создадим parse метод, который перебирает наши токены и проверяет, сформирован ли правильный синтаксис. Если это так, он предпримет необходимые действия.

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%sx1b[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)
  }
}

И вы посмотрите на это — у нас уже есть рабочий язык!

Скрин вывода терминала.

Хорошо, но иметь коды в строковой переменной не так уж и весело. Итак, давайте поместим наш фуксин коды в файле с именем code.m. Таким образом, мы можем отделить наши пурпурные коды от логики компилятора. мы используем .m как расширение файла, чтобы указать, что этот файл содержит код для нашего языка.

Давайте прочитаем код из этого файла:

// 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 rn 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()

Иди создавай язык программирования!

И с этим мы успешно создали крошечный язык программирования с нуля. Видите ли, язык программирования может быть таким же простым, как нечто, выполняющее одну конкретную задачу. Конечно, маловероятно, что такой язык, как Magenta, когда-либо будет достаточно полезен, чтобы стать частью популярного фреймворка или чего-то еще, но теперь вы видите, что нужно для его создания.

Небо действительно предел. Если вы хотите погрузиться немного глубже, попробуйте следовать этому видео, которое я сделал для более сложного примера. В этом видео я также показал, как вы также можете добавлять переменные в свой язык.

[Встраиваемое содержимое]
Spot_img

Последняя разведка

Spot_img

Чат с нами

Всем привет! Могу я чем-нибудь помочь?