Logo Zephyrnet

Hãy tạo một ngôn ngữ lập trình tí hon

Ngày:

Đến đây, có lẽ bạn đã quen thuộc với một hoặc nhiều ngôn ngữ lập trình. Nhưng bạn đã bao giờ tự hỏi làm thế nào bạn có thể tạo ra ngôn ngữ lập trình của riêng mình? Và ý tôi là:

Ngôn ngữ lập trình là bất kỳ tập hợp quy tắc nào chuyển đổi các chuỗi thành nhiều loại đầu ra mã máy khác nhau.

Tóm lại, một ngôn ngữ lập trình chỉ là một tập hợp các quy tắc được xác định trước. Và để làm cho chúng trở nên hữu ích, bạn cần một cái gì đó hiểu được các quy tắc đó. Và những thứ đó là trình biên dịch, phiên dịch, v.v. Vì vậy, chúng ta có thể đơn giản xác định một số quy tắc, sau đó, để làm cho nó hoạt động, chúng ta có thể sử dụng bất kỳ ngôn ngữ lập trình hiện có nào để tạo một chương trình có thể hiểu các quy tắc đó, đó sẽ là trình thông dịch của chúng ta.

Trình biên dịch

Một trình biên dịch chuyển đổi mã thành mã máy mà bộ xử lý có thể thực thi (ví dụ: trình biên dịch C ++).

Phiên dịch viên

Một trình thông dịch đi qua từng dòng chương trình và thực hiện từng lệnh.

Bạn có muốn thử không? Hãy cùng nhau tạo ra một ngôn ngữ lập trình siêu đơn giản để xuất ra đầu ra màu đỏ tươi trong bảng điều khiển. Chúng tôi sẽ gọi nó Magenta.

Ngôn ngữ lập trình đơn giản của chúng tôi tạo ra một biến mã chứa văn bản được in ra bảng điều khiển… tất nhiên là màu đỏ tươi.

Thiết lập ngôn ngữ lập trình của chúng tôi

Tôi sẽ sử dụng Node.js nhưng bạn có thể sử dụng bất kỳ ngôn ngữ nào để làm theo, khái niệm sẽ vẫn như cũ. Hãy để tôi bắt đầu bằng cách tạo một index.js tập tin và thiết lập mọi thứ.

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

Những gì chúng tôi đang làm ở đây là khai báo một lớp được gọi là Magenta. Lớp đó định nghĩa và khởi tạo một đối tượng chịu trách nhiệm ghi văn bản vào bảng điều khiển bằng bất kỳ văn bản nào mà chúng tôi cung cấp cho nó thông qua codes Biến đổi. Và, hiện tại, chúng tôi đã xác định rằng codes biến trực tiếp trong tệp với một vài thông báo "xin chào".

Ảnh chụp màn hình đầu ra của thiết bị đầu cuối.
Nếu chúng tôi chạy mã này, chúng tôi sẽ nhận được văn bản được lưu trữ trong các mã được đăng nhập trong bảng điều khiển.

OK, bây giờ chúng ta cần tạo một cái gọi là Lexer.

Lexer là gì?

OK, chúng ta hãy nói về ngôn ngữ tiếng Anh trong giây lát. Lấy cụm từ sau:

Làm thế nào là bạn?

Ở đây, “How” là trạng từ, “are” là động từ và “you” là đại từ. Chúng ta cũng có một dấu chấm hỏi (“?”) Ở cuối. Chúng ta có thể chia bất kỳ câu hoặc cụm từ nào như thế này thành nhiều thành phần ngữ pháp trong JavaScript. Một cách khác để chúng ta có thể phân biệt các phần này là chia chúng thành các token nhỏ. Chương trình chia văn bản thành các mã thông báo là Lexer.

Sơ đồ hiển thị lệnh đi qua lexer.

Vì ngôn ngữ của chúng ta rất nhỏ, nó chỉ có hai loại mã thông báo, mỗi loại có một giá trị:

  1. keyword
  2. string

Chúng tôi có thể đã sử dụng một biểu thức chính quy để trích xuất tokes từ codes chuỗi nhưng hiệu suất sẽ rất chậm. Một cách tiếp cận tốt hơn là lặp lại từng ký tự của code chuỗi và lấy mã thông báo. Vì vậy, hãy tạo một tokenize phương pháp trong của chúng tôi Magenta lớp - sẽ là Lexer của chúng ta.

Mã đầy đủ
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)
  }
}

Nếu chúng tôi chạy điều này trong một thiết bị đầu cuối với node index.js, chúng ta sẽ thấy danh sách các mã thông báo được in trong bảng điều khiển.

Ảnh chụp màn hình của mã.
Công cụ tuyệt vời!

Xác định các quy tắc và cú pháp

Chúng tôi muốn xem thứ tự các mã của chúng tôi có khớp với một số loại quy tắc hoặc cú pháp hay không. Nhưng trước tiên chúng ta cần xác định các quy tắc và cú pháp đó là gì. Vì ngôn ngữ của chúng ta rất nhỏ, nó chỉ có một cú pháp đơn giản là print từ khóa theo sau bởi một chuỗi.

keyword:print string

Vì vậy, hãy tạo một parse phương thức lặp qua các mã thông báo của chúng tôi và xem liệu chúng tôi có tạo cú pháp hợp lệ hay không. Nếu vậy, nó sẽ thực hiện các hành động cần thiết.

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

Và bạn có nhìn vào điều đó không - chúng tôi đã có một ngôn ngữ làm việc!

Ảnh chụp màn hình đầu ra của thiết bị đầu cuối.

Được rồi, nhưng có mã trong một biến chuỗi không phải là thú vị. Vì vậy, chúng ta hãy đặt của chúng tôi Magenta mã trong một tệp được gọi là code.m. Bằng cách đó, chúng ta có thể giữ cho các mã màu đỏ tươi của chúng ta tách biệt khỏi logic của trình biên dịch. Chúng tôi đang sử dụng .m dưới dạng phần mở rộng tệp để chỉ ra rằng tệp này chứa mã cho ngôn ngữ của chúng tôi.

Hãy đọc mã từ tệp đó:

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

Tạo một ngôn ngữ lập trình!

Và cùng với đó, chúng tôi đã tạo thành công một Ngôn ngữ lập trình nhỏ bé từ đầu. Hãy xem, một ngôn ngữ lập trình có thể đơn giản như một thứ gì đó hoàn thành một việc cụ thể. Chắc chắn, không có khả năng rằng một ngôn ngữ như Magenta ở đây sẽ đủ hữu ích để trở thành một phần của một khuôn khổ phổ biến hay bất cứ thứ gì, nhưng bây giờ bạn đã thấy những gì cần thiết để tạo ra một ngôn ngữ này.

Bầu trời thực sự là giới hạn. Nếu bạn muốn tìm hiểu sâu hơn một chút, hãy thử làm theo video này mà tôi đã thực hiện để xem xét một ví dụ nâng cao hơn. Đây là video tôi cũng đã cho bạn thấy, bạn cũng có thể thêm các biến vào ngôn ngữ của mình.

[Nhúng nội dung]
tại chỗ_img

Tin tức mới nhất

tại chỗ_img

Trò chuyện trực tiếp với chúng tôi (chat)

Chào bạn! Làm thế nào để tôi giúp bạn?