Lag din egen kalkulator - del 2 bugfix, ast og maskinkode

Nå er det på tide å fikse bug’en fra del 1. Kan ikke leve med sånt på samvittigheten. I tillegg syns jeg vi burde gjøre noe annet enn å bare spytte ut resultatet rett fra parseren. Det er mye mer gøy å regne ut svaret direkte i maskinkode!

Bugfix

I del 1 fikk vi feil svar når vi regnet ut 2 - 2 - 2. Problemet ligger i grammatikken:

 * expr -> term '+' expr | term '-' expr | term

Hvis vi bruker dette på eksemplet ovenfor får vi først en term som er 2, etterfulgt av -, og en ny expression. I praksis betyr dette at vi putter parenteser rundt tallene, omtrent slik: 2 - (2 - 2). Riktige parenteser skulle vært slik: (2 - 2) - 2. Dvs, hvis vi har flere minustegn etter hverandre må vi utøve de fra venstre til høyre, ikke fra høyre til venstre.

For å løse problemet må vi unngå at expr nevnes rekursivt i grammatikken. Det kan vi gjøre på følgende måte:

 * expr -> term ('+' / '-' term)*

Her sier vi enkelt og greit at en expression er en term etterfulgt av 0 eller flere nye terms med + eller - forran. I koden kan vi da utføre hver operasjon fra venstre til høyre, etterhvert som hver nye term kommer inn. Her er koden:

function expr(symbols) {
    const left = term(symbols)

    let symbol = symbols.shift()
    let result = left

    while(['plus', 'minus'].includes(symbol.type)){
        if (symbol.type === 'plus') result += term(symbols)
        if (symbol.type === 'minus') result -= term(symbols)
        symbol = symbols.shift()
    }

    symbols.unshift(symbol)
    return result
}

Basically det samme som vi hadde før, bare at nå har vi byttet ut det rekursivet kallet til expr med en while-loop.

Koden for term hadde det samme problemet, og løses på samme måte:

 * term -> factor ('*' / '/' factor)*
function term(symbols) {
    const left = factor(symbols)

    let symbol = symbols.shift()
    let result = left

    while(['multiply', 'divide'].includes(symbol.type)){
        if (symbol.type === 'multiply') result *= factor(symbols)
        if (symbol.type === 'divide') result /= factor(symbols)
        symbol = symbols.shift()
    }

    symbols.unshift(symbol)
    return result
}

Resten av koden er helt lik del 1.

Abstract syntax tree

For å komme videre må vi gjøre noe med outputen til parseren. Sånn det står nå regnes svaret ut direkte i parseren. Men hva hvis vi vil regne ut svaret i maskinkode? Det vi trenger er et abstract syntax tree. I stedet for å regne ut svaret direkte lager vi heller en trestruktur som beskriver regnestykket vårt. Denne kan vi igjen bruke til å generere maskinkode for ulike maskiner.

Her er koden, skrevet om til å spytte ut en AST:

function expr(symbols) {
    const left = term(symbols)

    let symbol = symbols.shift()
    let root = left

    while(['plus', 'minus'].includes(symbol.type)){
        root = {
            type: 'expr',
            operator: symbol.type,
            left: root,
            right: term(symbols)
        }
        symbol = symbols.shift()
    }

    symbols.unshift(symbol)
    return root
}

function term(symbols) {
    const left = factor(symbols)

    let symbol = symbols.shift()
    let root = left

    while(['multiply', 'divide'].includes(symbol.type)){
        root = {
            type: 'term',
            operator: symbol.type,
            left: root,
            right: factor(symbols)
        }
        symbol = symbols.shift()
    }

    symbols.unshift(symbol)
    return root
}

function factor(symbols) {
    const symbol = symbols.shift()

    if (symbol.type === 'lparen') {
        const result = expr(symbols)
        const rparen = symbols.shift()
        if (rparen.type !== 'rparen') {
            throw new Error('Expected right parens')
        }
        return result
    }

    if (symbol.type === 'number') return { type: 'number', value: symbol.value }
    if (symbol.type === 'plus') return factor(symbols)
    if (symbol.type === 'minus') return { type: 'term', operator: 'multiply', left: { type: 'number', value: -1}, right: factor(symbols) }

    throw new Error('Unexpected symbol ' + symbol.type)
}

For eksempelet vårt (2 - 2 - 2) får vi følgende output:

{
  type: 'expr',
  operator: 'minus',
  left: {
    type: 'expr',
    operator: 'minus',
    left: { type: 'number', value: 2 },
    right: { type: 'number', value: 2 }
  },
  right: { type: 'number', value: 2 }
}

Vi har altså et sett med noder. Hver node har en type (‘expr’, ‘term’, ‘number’). Hvis noden er helt i bunn av treet er den en “number” og har bare en “value”. Hvis noden er høyere opp i treet har den typisk en venstre og høyreside med en operator i mellom.

Nå som vi har en AST kan vi bruke denne videre til å gjøre mye artig. Hvis dette var et programmeringsspråk kunne vi gjort optimaliseringer på treet. Vi kunne beskjært treet, kollapset greiner, analysert for å finne feil, mm.

Maskinkode

Ok, her må jeg innrømme at jeg er ganske blank. Det er heller ikke noe poeng å gjøre om et regnestykke til maskinkode når vi allerede er i et programmeringsspråk som kan gi oss svaret direkte. Men whatevs, vi kjører på.

Jeg plukka meg ut en maskin til å kjøre programmet vårt. Det er en 8 bits emulator som kan testes her: https://schweigi.github.io/assembler-simulator/

Den har noen flerbruksregistre A, B, C, D, som kan brukes til å lagre verdier. Den har også en stack som du kan pushe og poppe fra. Den skal vi utnytte til det fulle. Et par skjær i sjøen er at den ikke støtter negative tall eller desimaltall osv (eller jeg har i hvert fall ikke funnet ut av det enda). Så vi får holde oss til veldig enkle regnestykker :stuck_out_tongue:

Her er koden:

function codegen(node) {
    if(node.type === 'expr'){
        return genExpr(node)
    } else if (node.type === 'number') {
        return genNumber(node)
    } else if (node.type === 'term') {
        return genTerm(node)
    }
}

function genExpr(node) {
    if (node.operator === 'plus') {
        return [
            codegen(node.left),
            codegen(node.right),
            `pop B`,
            `pop A`,
            `add A, B`,
            `push A`,
        ].join('\n')
    } else if (node.operator === 'minus') {
        return [
            codegen(node.left),
            codegen(node.right),
            `pop B`,
            `pop A`,
            `sub A, B`,
            `push A`,
        ].join('\n')
    }
}

function genTerm(node) {
    if (node.operator === 'multiply') {
        return [
            codegen(node.left),
            codegen(node.right),
            `pop B`,
            `pop A`,
            `mul B`,
            `push A`,
        ].join('\n')
    } else if (node.operator === 'divide') {
        return [
            codegen(node.left),
            codegen(node.right),
            `pop B`,
            `pop A`,
            `div B`,
            `push A`,
        ].join('\n')
    }
}

function genNumber(node) {
    return `push ${node.value}`
}

Output for 2 + 2 + 2 blir (test her: https://schweigi.github.io/assembler-simulator):

push 2
push 2
pop B
pop A
add A, B
push A
push 2
pop B
pop A
add A, B
push A

Så ideen her er at hvert nivå i syntax-treet vårt dytter resultatet sitt på stacken. Nivået over kan poppe verdien av stacken og putte den i et register.

For eksempel, i en expression genererer man koden for venstre og høyreside, og begge disse pusher resultatet sitt på stacken:

codegen(node.left),
codegen(node.right),
`pop B`,
`pop A`,
`add A, B`,
`push A`,

Man popper begge disse verdiene av stacken i hvert sitt register (A og B). Deretter kan man kjøre ADD-instruksjonen på disse registrene. Resultatet havner i register A, som vi så dytter på stacken igjen.

Oppsummering

Igjen, dette var ganske meningsløst, men kanskje litt artig også. Det var i hvert fall litt artig å generere denne “maskinkoden”, det har jeg aldri prøvd før. I x86 kan man gjøre artimetikk direkte i assembly, så i praksis ville man vel aldri sett den løsninga jeg presenterte her. De fleste kompilatorer ville også optimalisert vekk hele regnestykket og byttet det ut med resultatet. Men prinsippene er i hvert fall de samme. Neste steg nå blir å lage et helt eget programmeringsspråk!

2 Likes