Vid det här laget är du förmodligen bekant med ett eller flera programmeringsspråk. Men har du någonsin undrat hur du kan skapa ditt eget programmeringsspråk? Och med det menar jag:
Ett programmeringsspråk är vilken uppsättning regler som helst som konverterar strängar till olika typer av maskinkod.
Kort sagt, ett programmeringsspråk är bara en uppsättning fördefinierade regler. Och för att göra dem användbara behöver du något som förstår dessa regler. Och det är de sakerna kompilatorer, tolkar, etc. Så vi kan helt enkelt definiera några regler, sedan, för att få det att fungera, kan vi använda vilket befintligt programmeringsspråk som helst för att skapa ett program som kan förstå dessa regler, vilket kommer att vara vår tolk.
Kompilator
En kompilator omvandlar koder till maskinkod som processorn kan exekvera (t.ex. C++ kompilator).
Tolk
En tolk går igenom programmet rad för rad och utför varje kommando.
Vill du ge det ett försök? Låt oss skapa ett superenkelt programmeringsspråk tillsammans som matar ut magentafärgad utdata i konsolen. Vi kallar det Magenta.
Konfigurera vårt programmeringsspråk
Jag kommer att använda Node.js men du kan använda vilket språk som helst för att följa med, konceptet kommer att förbli detsamma. Låt mig börja med att skapa en index.js
fil och ställ in saker.
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()
Det vi gör här är att förklara en klass kallad Magenta
. Den klassen definierar och initierar ett objekt som är ansvarigt för att logga text till konsolen med vilken text vi än tillhandahåller den via en codes
variabel. Och för tillfället har vi definierat det codes
variabel direkt i filen med ett par "hej"-meddelanden.
OK, nu måste vi skapa en vad som kallas en Lexer.
Vad är en Lexer?
Okej, låt oss prata om det engelska språket för en sekund. Ta följande fras:
Hur mår du?
Här är "Hur" ett adverb, "är" är ett verb och "du" är ett pronomen. Vi har också ett frågetecken ("?") i slutet. Vi kan dela upp vilken mening eller fras som helst som denna i många grammatiska komponenter i JavaScript. Ett annat sätt vi kan särskilja dessa delar är att dela upp dem i små tokens. Programmet som delar upp texten i polletter är vårt lexer.
Eftersom vårt språk är väldigt litet har det bara två typer av tokens, var och en med ett värde:
keyword
string
Vi kunde ha använt ett reguljärt uttryck för att extrahera tokes från codes
sträng men framförandet kommer att vara mycket långsamt. Ett bättre tillvägagångssätt är att gå igenom varje karaktär i code
snöre och grepppoletter. Så låt oss skapa en tokenize
metod i vår Magenta
klass — som kommer att bli vår Lexer.
Fullständig kod
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)
}
}
Om vi kör detta i en terminal med node index.js
, bör vi se en lista över tokens utskrivna i konsolen.
Definiera regler och syntaxer
Vi vill se om ordningen på våra koder matchar någon sorts regel eller syntax. Men först måste vi definiera vad dessa regler och syntaxer är. Eftersom vårt språk är så litet har det bara en enkel syntax som är en print
nyckelord följt av en sträng.
keyword:print string
Så låt oss skapa en parse
metod som går igenom våra tokens och ser om vi har en giltig syntax bildad. Om så är fallet kommer den att vidta nödvändiga åtgärder.
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)
}
}
Och skulle du titta på det — vi har redan ett arbetsspråk!
Okej men att ha koder i en strängvariabel är inte så kul. Så låt oss sätta vår Magenta koder i en fil som heter code.m
. På så sätt kan vi hålla våra magentafärgade koder åtskilda från kompilatorlogiken. Vi använder .m
som filtillägg för att indikera att den här filen innehåller kod för vårt språk.
Låt oss läsa koden från den filen:
// 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, "")
const magenta = new Magenta(codes)
magenta.run()
Gå och skapa ett programmeringsspråk!
Och med det har vi framgångsrikt skapat ett litet programmeringsspråk från grunden. Se, ett programmeringsspråk kan vara så enkelt som något som åstadkommer en specifik sak. Visst, det är osannolikt att ett språk som Magenta här någonsin kommer att vara användbart nog för att vara en del av ett populärt ramverk eller något, men nu ser du vad som krävs för att göra ett.
Himlen är verkligen gränsen. Om du vill dyka in lite djupare, prova att följa den här videon som jag gjorde genom att gå igenom ett mer avancerat exempel. Det här är videon som jag också har visat hur du kan lägga till variabler till ditt språk också.