Հաշվարկիչ կամ արտահայտությունների ինտերպրետատոր
Ինչպե՞ս գրել տողով տրված մաթեմատիկական արտահայտությունը հաշվարկող ծրագիր։ Կարող է թվալ, թե ավելորդ է գրել այս թեմայով, քանի որ կարելի է գտնել բազմաթիվ ու բազմազան գրականություն՝ հարուստ տեսական ու գործնական նյութով։ Բայց ես որոշեցի սկսել մի շարք, որտեղ կպատմեմ, թե ինչպես նախ կառուցել մի քանի թվաբանական գործողություններ կատարող հաշվարկիչ, ապա դրա հենքի վրա՝ քիչ-քիչ ընդլայնելով, ստեղծել ծրագրավորման լեզվի ինտերպրետատոր։
Որպես իրականացման միջոց ընտրել եմ Go լեզուն՝ երկու պատճառով. ա) ես ուզում եմ ավելի խորն ուսումնասիրել այդ լեզուն, և բ) ուզում եմ իմ կուտակած փորձը կիսել նրանց հետ, ովքեր հետաքրքրվում են Go լեզվով։
Սահմանումներ
Ամենից առաջ պետք է որոշել, թե ի՞նչ ֆունկցիոնալությամբ է օժտված լինելու հաշվարկիչը։ Սկզբի համար ես ընտրել եմ թվաբանական չորս բինար գործողություններ՝ գումարում, հանում, բազմապատկում և բաժանում, մեկ ունար գործողություն՝ բացասում, խմբավորման փակագծեր՝ գործողություններ։ Հաշվարկիչն աշխատելու է մեծ (“անսահմանափակ” երկարությամբ) ամբողջ թվերի հետ։ Օգտագործողի տեսակետից հաշվարկիչը ստանալու է արտահայտության տեքստային ներկայացումը, հաշվելու է նրա արժեքը և վերադարձնելու է պատասխանը՝ նորից տեքստային ներկայացմամբ։
EBNF գրառմամբ հաշվարկիչի լեզուն ներկայացնող քերականությունը կունենա այսպիսի տեսք.
Factor = "Number" | "-" Factor | "(" Expression ")". Term = Factor {("*" | "/") Factor}. Expression = Term {("+" | "-") Term}.
Այս երեք կանոններն ամբողջությամբ նկարագրում են գործողությունների թե՛ առաջայնությունը և թե՛ ասոցիատիվությունը։
Աբստրակտ քերականական ծառ
Քերականությամբ նկարագրված արտահայտությունների մոդելը աբստրակտ քերականական ծառն է (abstract syntax tree, AST)։ Այդ ծառի հանգույցները ներկայացնում են արտահայտության գործողությունները, իսկ տերևները ներկայացնում են տվյալները։ Օրինակ, 123 + 45 *
6 արտահայտության համար պետք է կառուցել հետևյալ ծառը.
+ / \ / \ 123 * / \ 45 6
Աբստրակտ քերականական ծառը կառուցվում է քերականական անալիզատորի (parser) կողմից, և օգտագործվում է արտահայտության ինտերպրետացիայի (կամ կոմպիլյացիայի) ժամանակ։
Go լեզվով սահմանենք asteval
փաթեթը, որը տրամադրում է ծառի հանգույցների ստրուկտուրան, կոնստրուկտոր ֆունկցիաներ՝ հանգույցներ ու տերևներ ստեղծելու համար, ինչպես նաև ծառի ինտերպրետացիայի ֆունկցիան։ Պրոյեկտի src
պանակում ստեղծենք asteval
պանակ, իսկ նրա մեջ՝ asteval.go
ֆայլը։
Հայտարարենք asteval
փաթեթը.
package asteval
Քանի որ մեր արտահայտություններն օգտագործելու են մեծ ամբողջ թվեր, Go լեզվի գրադարանից ներմուծենք big
փաթեթը.
import "math/big"
Սահմանենք number = 0
, unary = 1
, binary = 2
հաստատունները, որոնք ցույց են տալու ծառի հանգույցի տիպը.
const ( number byte = iota unary binary )
Ծառի հանգույցը սահմանենք որպես Expression
անունով կառուցվածք։ Նրա class
դաշտը ցույց է տալիս հանգույցի տիպը, value
դաշտը ցույց է տալիս հանգույցի արժեքը, եթե տիպը number
է, operation
դաշտը ցույց է տալիս հանգույցում ներկայացված ունար կամ բինար գործողությունը, իսկ first
և second
դաշտերը ցույց են տալիս հանգույցի ենթածառերը։ Եթե class = unary
, ապա second = nil
։
type Expression struct { class byte value *big.Int operation rune first, second *Expression }
Ինչպես նշվեց, մենք գործ ենք ունենալու երեք տիպի հանգույցների հետ՝ թիվ, ունար գործողություն և բինար գքործողություն։ Սահմանենք կոնստրուկտոր ֆունկցիաներ նրանց համար։
Թիվ (number) ներկայացնող կոնստրուկտերը արգումենտում ստանում է թվի տեքստային տեսքը, և վերադարձնում է Expression
կառուցվածքի նոր ստեղծված նմուշի ցուցիչը, որի class
դաշտին վերագրված է number
հաստատունը, իսկ value
դաշտին՝ big.Int
օբյեկտի ցուցիչը։
func Number(num string) *Expression { nm := new(big.Int) nm.SetString(num, 10) return &Expression{class: number, value: nm} }
ՈՒնար գործողությանը համապատասխանող հանգույց ստեղծելու համար պետք է տալ գործողության նիշը և ենթաարտահայտությունը։ Չնայաց, որ մեր դեպքում կա միայն մեկ ունար գործողություն՝ բացասումը, կոնստրուկտոր ֆունկցիան որպես առաջին արգումենտ ընդունում է նաև գործողության նշանը։
func Unary(op rune, ex *Expression) *Expression { return &Expression{class: unary, operation: op, first: ex} }
Բինար գործողության հանգույցի համար էլ պետք է ունենալ գործողության նշանը, աջ ու ձախ ենթաարտահայտությունները։
func Binary(op rune, exo, exi *Expression) *Expression { return &Expression{class: binary, operation: op, first: exo, second: exi} }
Արտահայտության արժեքը հաշվելու համար պետք է անցում կատարել նրա համար կառուցած աբստրակտ քերականական ծառով՝ ըստ հետևյալ կանոնների.
-
եթե
class = number
, ապա վերադարձնել նրաvalue
դաշտը, -
եթե
class = unary
, ապա այս կանոնների կիրառմամբ հաշվելfirst
ենթաարտահայտությունը և այդ արժեքի վրա կիրառելoperation
դաշտով որոշվող գործողությունը։ -
եթե
class = binary
, ապա այս կանոնների կիրառմամբ հաշվելfirst
ևsecond
ենթաարտահայտությունները, ապա ստացված արժեքների նկատմամբ կիրառելoperation
դաշտով որոշվող գործողությունը։
Նշված կանոններն իրականացնելու համար Expression
կառուցվածքի ցուցիչի համար սահմանենք Evaluate
մեթոդը։
func (e *Expression) Evaluate() *big.Int { var res *big.Int = new(big.Int) switch e.class { case number: res.Set(e.value) case unary: res = e.first.Evaluate() if '-' == e.operation { res.Neg(res) } case binary: exo := e.first.Evaluate() exi := e.second.Evaluate() switch e.operation { case '+': res.Add(exo, exi) case '-': res.Sub(exo, exi) case '*': res.Mul(exo, exi) case '/': res.Div(exo, exi) } } return res }
Ակնհայտ է, որ այս ֆունկցիան ունի թերություն. այն բաժանման գործողությունը կատարելուց առաջ չի ստուգում երկրորդ արգումենտի զրո լինելը։ Բայց այս անգամ անտեսենք այդ թերությունը՝ հետագայում ճշտելու պայմանով։
Ահա և վերջ, ամբողջությամբ սահմանված է asteval
փաթեթը։ Այն տրամադրում է Expression
կառուցվածքը, Number
, Unary
, Binary
կոնստրուկտորները և Evaluate
մեթոդը։
Լեքսիկսկան և քերականական վերլուծություն
Աբստրակտ քերականական ծառն արտահայտության ներքին, օգտագործողին չերևացող մասն է։ Տեքստային ներկայացումից այն ստանալու համար պետք է արտահայտությունը ենթարկել քերականական վերլուծության, և այդ վերլուծությունը պետք է կատարել վերը բերված կանոններով։ Բայց քերականական վերլուծության համար պետք է նախ արտահայտության տեքստը տրոհել տերմինալային տրամաբանական կտորների՝ լեքսեմների և ամենի մի լեքսեմի հետ կապել նրա տիպը (նշանակությունը) ցույց տվող հատկություն՝ թոքեն։ Օրինակ, 123 + 45 *
6 արտահայտության համար տրոհումը կարող է ունենալ այսպիսի տեսք.
Lexeme | Token |
---|---|
"123 | Number |
“+” | Add |
“45” | Number |
“* ” |
Mul |
“6” | Number |
Լեքսիկական անալիզատորի խնդիրն է հենց այս տրոհումը։ Քերականական անալիզատորն օգտագործում է թոքենները քերականական կանոնն ընտրելու համար, իսկ լեքսեմներն օգտագործում է ծառի հանգույցների պարունակությունը լրացնելու համար։
scanner
փաթեթը
Լեքսիկական անլիզատորն իրականացնենք scanner
փաթեթում։
package scanner
Ընթերցվող սիմվոլների տիպը որոշելու համար ներմուծենք Go լեզվի գրադարանային unicode
փաթեթը։
import "unicode"
Սահմանենք Scanner
կառուցվածքը, որի text
դաշտը արտահայտության տեքստի նիշերի զանգվածն է, իսկ pos
ինդեքսը ցույց է տալիս հերթական ընթերցվող սիմվոլը։
type Scanner struct { text []rune pos int }
Scanner կառուցվածքի կոնստրուկտորը ստանում է վերլուծության ենթակա արտահայտության տեքստը, նրա վերջից կցում է “;
” նիշը որպես արտահայտության ավարտի նշան, ապա տողը վերածում է նիշերի վեկտորի և վերագրում text
դաշտին։
func New(txt string) *Scanner { return &Scanner{[]rune(txt+";"), 0} }
Թոքենների համար սահմանենք հաստատուններ.
const ( None byte = iota Number // Digit{Digit} Add // '+' Sub // '-' Mul // '*' Div // '/' LPar // '(' RPar // ')' Eos // ';' )
Սահմանենք մի օգնական մեթոդ, որը արտահայտության նիշերի շարքից կարդում է տրված պրեդիկատին բավարարող նիշերի անընդհատ շարք և վերադարձնում է դրանցից կազմած տողը։
func (s* Scanner) scanWith(predicate func(r rune) bool) string { k := s.pos for predicate(s.text[s.pos]) { s.pos++ } return string(s.text[k:s.pos]) }
Scanner
կառուցվածքի Next
մեթոդը վերադարձնում է երկու արժեք՝ հերթական լեքսեմը և նրան համապատասխանող թոքենը։ Մեթոդը նախ կանչում է scanWith
մեթոդը IsSpace
պրեդիկատով, որպեսզի դեն նետի բացատնանիշերը։ Այնուհետև, եթե հերթական նիշը թվանշան է, ապա scanWith
մեթոդը կանչում է IsDigit
պրեդիկատով ստանում է ամբողջ թիվ կազմող թվանշանների շարք։ Այս դեպքում վերադարձվում է թվանշաններից կազմված տողը որպես լեքսեմ և Number
հաստատունը որպես թոքեն։ Թվանշաններից բացի դիտարկվում են միայն ‘+
’, ‘-
’, ‘*
’, ‘/
’, ‘(
’, ‘)
’ և ‘;
’ նիշերը և ամեն մեկի համար վերադարձվում է համապատասխան թոքենը։ Բոլոր այլ նիշերի համար վերադարձվում է None թոքենը։
func (s *Scanner) Next() (string, byte) { s.scanWith(unicode.IsSpace) ch := s.text[s.pos] if unicode.IsDigit(ch) { return s.scanWith(unicode.IsDigit), Number } var res byte = None s.pos++ switch ch { case '+': res = Add case '-': res = Sub case '*': res = Mul case '/': res = Div case '(': res = LPar case ')': res = RPar case ';': res = Eos } return string(ch), res }
Սա էլ լեքսիկական անալիզատորի փաթեթեը։ նորից ոչ մի սխալների մշակում չի կարարված զուտ պարզության համար։ Նախագծի հետագա զարգացումներում անպայմանորեն պետք է հաշվի առնել հնարավոր սխալները։
parser
փաթեթը
Քերականական անալիզատորն իրականացված է parser
փաթեթում։ Այն ըստ քերականական կանոնների վերլուծում է արտահայտության տեքստը և կառուցում է աբստրակտ քերականական ծառը։ Քերականական սխալները չեն մշակվում՝ նորից պարզության համար։
package parser
Ներմուծենք մեր նախապես ստեղծած երկու փաթեթները։
import ( "scanner" "asteval" )
Parser
կառուցվածքը քերականական անալիզատորի մոդելն է։ Նրա look
դաշտը ընթացիկ թոքենն է՝ look-a-head սիմվոլը, lexeme
դաշտը ընթացիկ լեքսեմն է, scan
դաշտը լեքսիկական անալիզատորի ցուցիչն է։
type Parser struct { look byte lexeme string scan *scanner.Scanner }
Parser կառուցվածքի համար կոնստրուկտոր չենք գրել, քանի որ դրա կարիքը պարզապես չկա։
Միակ միջոցը, որով քերականական անալիզատորը շփվում է արտաքին աշխարհի հետ, դա Parse
մեթոդն է։ Այն ստանում է արտահայտության տեքստը և վերադարձնում է աբստրակտ քերականական ծառի արմատը (եթե վերլուծության ընթացքում սխալներ չեն հայտնաբերվել)։
func (p* Parser) Parse(src string) *asteval.Expression { p.look = scanner.None p.scan = scanner.New(src) p.match(scanner.None) return p.expr() }
Parser
-ը լեքսիկական անալիզատորից հերթական լեքսեմ-թոքեն զույգը կարդում է match
մեթոդի օգնությամբ։ Հետագայում այս մեթոդի մեջ է իրականացվելու որոշ քերականական սխալների մասին ազդարարումը։
func (p *Parser) match(tok byte) bool { if tok != p.look { return false } p.lexeme, p.look = p.scan.Next() return true }
Դիտարկվող արտահայտություների քերականությունն ունի երեք արտածման կանոն, ըստ որոնց էլ կառուցել եմ քերականական անալիզատորի մեթոդները։
Առաջինը Factor
կանոնն է, որն ունի արտածման երեք տարբերակ․
Factor = "Number". Factor = "-" Factor. Factor = "(" Expression ")".
ՈՒշադիր նայելով այս արտածման կանոններին տեսնում ենք, որ Factor
կանոնը կարելի է կիրառել միայն այն դեպքում, երբ լեքսիկական անալիզատորի look-a-head սիմվոլը “Number
”, “-
”, “(
” թոքեններից որևէ մեկն է։
Եթե look-a-head = “Number
”, ապա ստեղծում և վերադարձնում ենք ծառի նոր տերև՝ ընթացիկ լեքսեմի արժեքով։ Եթե look-a-head = “-
”, ապա հանդիպել ենք ունար գործողության։ Ռեկուրսիվ կանչով կառուցում ենք գործողության արգումենտի ծառը, ապա ստեղծում ենք ծառի ունար գործողության հանգույց։ Եթե look-a-head = “(
”, ապա ռեկուրսիվ կանչով կառուցում ենք փակագծերի ներսում գրված արտահայտության ծառը, ապա match
մեթոդի կանչով համոզվում ենք, որ առկա է փակվող փակագիծը։
func (p *Parser) factor() *asteval.Expression { // number if p.look == scanner.Number { num := p.lexeme p.match(scanner.Number) return asteval.Number(num) } // unary operation '-' if p.look == scanner.Sub { p.match(scanner.Sub) ex := p.factor() return asteval.Unary('-', ex) } // expression in parentheses if p.look == scanner.LPar { p.match(scanner.LPar) ex := p.expr() p.match(scanner.RPar) return ex } return nil }
Քերականության հաջորդ կանոնը կառուցում է բազմապատկման ու բաժանման բինար գործողություններին համապատասխանող հանգույցները։ Ըստ քերականական երկրորդ կանոնի, Term-ը կարող է լինել կա՛մ Factor, կա՛մ “*
” և “/
” նիշերով իրար կցված Factor-ների հաջորդականություն․
Term = Factor {("*" | "/") Factor}.
Քերականական անալիզատորի term
մեթոդը այս կանոնի տառացի իրականացումն է։
func (p *Parser) term() *asteval.Expression { ex1 := p.factor() for p.look == scanner.Mul || p.look == scanner.Div { op := rune(p.lexeme[0]) p.match(p.look) ex2 := p.factor() ex1 = asteval.Binary(op, ex1, ex2) } return ex1 }
Երրորդ քերականական կանոնը կառուցում է գումարման ու հանման բինար գործողություններին համապատասխանող հանգույցները։ expr
մեթոդը շատ նման է term
մեթոդին։ Քանի որ մեր քննարկած արտահայտություններում գումարման ու հանման գործողություններն ունեն ամենացածր նախապատվությունը, հենց այս մեթոդն է կանչվում Parse
մեթոդից։
func (p *Parser) expr() *asteval.Expression { ex1 := p.term() for p.look == scanner.Add || p.look == scanner.Sub { op := rune(p.lexeme[0]) p.match(p.look) ex2 := p.term() ex1 = asteval.Binary(op, ex1, ex2) } return ex1 }
Երկխոսություն օգտագործողի հետ
Հաշվարկիչի և օգտագործողի շփումն իրականացված է հրամանային տողի օգնությամբ։ Գործարկումից հետո ծրագիրն արտածում է “>
” սիմվոլը և սպասում է, որ օգտագործողը ներմուծի արտահայտության տեքստը։ Արտահայտության ներմուծումից և Enter ստեղնի սեղմումից հետո հաշվարկվում և արտածվում է արտահայտությունը արժեքը։
Փաթեթն անվանված է repl
՝ (Read-Evaluate-Print-Loop)։
package repl
Ներմուծենք անհրաժեշտ փաթեթները։
import ( "fmt" "os" "bufio" "parser" )
Եվ միակ Run
պրոցեդուրան, որը reader
-ի միջոցով կարդում է օգտագործողի տված տեքստը, parser
-ի միջոցով վերլուծում է այն ու կառուցում է աբստրակտ քերականական ծառ։ Այնուհետև ծառի արմատի համար կանչելով Evaluate
մեթոդը՝ հաշվում է արտահայտության արժեքը։ Ցիկլը կատարվում է այնքան ժամանակ, քանի դեռ օգտագործողի ներածած տեքստի առաջին նիշը “.
” (կետ) չէ։
func Run() { reader := bufio.NewReader(os.Stdin) parser := new(parser.Parser) for { fmt.Printf("> ") sr, _ := reader.ReadString('\n') ex := string(sr) if '.' == ex[0] { break } ast := parser.Parse(ex) res := ast.Evaluate() fmt.Println(">", res.String()) } }
Ծրագրի մուտքի կետը և գործարկումը
Go լեզվով գրված ծրագրերի մուտքի կետ է հանդիսանում main
փաթեթի main
ֆունկցիան։ Այն պարզապես կանչում է repl
փաթեթի Run ֆունկցիան։
package main import "repl" func main() { repl.Run() }
Հաշվարկիչ վերագրման և արտածման հրամաններով
Այս անգամ ես ներկայացնում եմ հաշվարկիչի մի նոր զարգացում, որտեղ ավելացված են փոփոխականին արժեքի վերագրման և արտահայտության արժեքի արտածման հրամանները։ Ի տարբերություն միայն թվաբանական գործողություններ կատարող հաշվարկիչի, փոփոխականներով հաշվարկիչը հնարավորություն է տալիս պահպանել և կրկին օգտագործել հաշվարկման արդյունքները։ Օրինակ, կարելի է գրել հետևյալ հրամանները․
a = 5 b = 4 print a + 1 + b
որոնց կատարումից հետո հաշվարկիչը կարտածի 10 արդյունքը։
Պետք է նկատի ունենալ, որ վերագրման և արտածման գործողությունները հրամաններ են, այլ ոչ թե արտահայտություններ։
Նոր քերականությունը
Հրամանների ավելացմամբ հաշվարկիչի լեզվի քերականությունն ընդլայնվում է և ստանում է ահա այսպիսի տեսք։
Statement = Assignment | Print. Assignment = 'Ident' '=' Expr. Print = 'print' Expr. Expr = Term {('+' | '-') Term}. Term = Factor {('*' | '/') Factor}. Factor = '(' Expr ')' | '-' Factor | 'Number' | 'Ident'.
Կատարման միջավայր
Քանի որ հաշվարկիչը պետք է կարողանա հիշել փոփոխականների արժեքները, սահմանենք Environment
կառուցվածքը, որը մասնակցելու է հրամանների կատարման և արտահայտությունների հաշվարկման պրոցեսին և որի միջոցով փոխանցվելու են փոփոխականների արժեքները։
package environ import "math/big"
Environment
կառուցվածքը պարունակում է data
դաշտը, որը արտապատկերում է փոփոխականի անունները արժեքներին։ Արտապատկերումը կազմակերպելու համար օգտագործված է Go լեզվի map
օբյեկտը։
type Environment struct { data map[string]*big.Int }
Միջավայրի կոնստրուկտորը պարզապես ստեղծում և վերադարձնում է նոր Environment
օբյեկտի ցուցիչ։
func New() *Environment { return &Environment{data: make(map[string]*big.Int)} }
Փոփոխականների արժեքները միջավայրում ավելացվում կամ թարմացվում են Set
մեթոդով։
func (e *Environment) Set(name string, value *big.Int) { e.data[name] = value }
Որևէ փոփոխականի արժեքը միջավայրից ստանալու համար է նախատեսված Get
մեթոդը։
func (e *Environment) Get(name string) *big.Int { value, _ := e.data[name] return value }
Աբստրակտ քերականական ծառի ընդլայոնում
Քերականության Factor արտածման կանոնում ավելացել է նոր ճյուղ՝ 'Ident'
, որը ցույց է տալիս, որ փոփոխականները նույնպես արտահայտություն են։ Ընդլայնենք asteval
փաթեթի Expression
կառուցվածքն այնպես, որ այն սպասարկի նաև փոփոխականները։
type Expression struct { class byte value *big.Int varname string operation rune first, second *Expression }
Իսկ արտահայտության տիպը որոշող հաստատունների ցուցակում ավելացնենք ևս մեկ հաստատուն՝ variable
։
const ( number byte = iota variable unary binary )
Բնականաբար պետք է ունենալ նաև նոր կոնստրուկտոր, որը կառուցում է փոփոխականին համապատասխան հանգույց.
func Variable(nam string) *Expression { return &Expression{class: variable, varname: nam} }
Փոփոխության է ենթարկվում նաև արտահայտության արժեքը հաշվարկող Evaluate
մեթոդը։ Այժմ այն իր արգումենտում ստանում է հաշվարկման միջավայրը՝ Environment
կառուցվածքի ցուցիչ, որից վերցնելու ենք փոփոխականի արժեքը։
func (e *Expression) Evaluate(env *environ.Environment) *big.Int { var res *big.Int = new(big.Int) switch e.class { case number: res.Set(e.value) case variable: res.Set(env.Get(e.varname)) case unary: res = e.first.Evaluate(env) if '-' == e.operation { res.Neg(res) } case binary: exo := e.first.Evaluate(env) exi := e.second.Evaluate(env) switch e.operation { case '+': res.Add(exo, exi) case '-': res.Sub(exo, exi) case '*': res.Mul(exo, exi) case '/': res.Div(exo, exi) } } return res }
Լեզվի քերականության մեջ ավելացել է հրամանի հասկացությունը՝ Statement
, իր երկու ներկայացումներով՝ Assignment
և Print
։ Հրամանների աբստրակտ քերականական ծառը կառուցելու համար ստեղծենք նոր փաթեթ.
package astexec import ( "asteval" "fmt" "environ" )
Սահմանենք Statement
ինտերֆեյսը, որի Execute
մեթոդով իրականացնելու ենք տարբեր հրամանների վարքը.
type Statement interface { Execute(env *environ.Environment) }
Վերագրման հրամանն իրականացված է Assignment
կառուցվածքով, որի name
դաշտը փոփոխականի անունն է, իսկ expr
դաշտը այն արտահայտությունն է, որի արժեքը միջավայրում պետք է կապել փոփոխականի հետ։
type Assignment struct { name string expr *asteval.Expression }
Կոնստրուկտորը շատ պարզ է։
func NewAssignment(nm string, ex *asteval.Expression) *Assignment { return &Assignment{name: nm, expr: ex} }
Execute
մեթոդի իրականացումը նախ հաշվում է expr
արտահայտության արժեքը, ապա միջավայրում ավելացնում է նոր համապատասխանություն։
func (a *Assignment) Execute(env *environ.Environment) { val := a.expr.Evaluate(env) env.Set(a.name, val) }
Արտածման հրամանի միակ expr
դաշտը այն արտահայտությունն է, որի արժեքը պետք է հաշվել ու արտածել։
type Print struct { expr *asteval.Expression }
Print
հրամանի կոնստրուկտորն է.
func NewPrint(ex *asteval.Expression) *Print { return &Print{expr: ex} }
Իսկ արտածման հրամանի կատարման համար պետք է պարզապես հաշվել expr
արտահայտության արժեքը և fmt.Println
ֆունկցիայով արտածել ստանդարտ արտածման հոսքի վրա։
func (p *Print) Execute(env *environ.Environment) { val := p.expr.Evaluate(env) fmt.Println(val.String()) }
Փոփոխություններ լեքսիկական անալիզատորում
Քերականության մեջ ավելացել են երեք թոքեններ՝ “Ident”, “Print” և “=”։ Իդենտիֆիկատորը դա տառով սկսվող տառաթվային հաջորդականություն է։ ‘print’-ը արտածման հրամանի ծառայողական բառն է։ ‘=’ նիշը վերագրման հրամանի սիմվոլն է։
const ( None byte = iota Number // Digit{Digit} Ident // Letter{Letter|Digit} Add // '+' Sub // '-' Mul // '*' Div // '/' LPar // '(' RPar // ')' Equal // '=' Print // 'print', '?' Eos // '@' )
Ծառայողական բառերի համար սահմանենք մի աղյուսակ, որում իդենտիֆիկատորին համապատասխանեցված է թոքեն։ Այս աղյուսակն օգտագործելու ենք, որպեսզի պարզենք արդոք կարդացած իդենտիֆիկատորը ծառայողական բառ է, թե՝ ոչ։
var keywords = map[string]byte { "print": Print }
Սահմանենք նաև մի օգնական ֆունկցիա, որը դրական պատասխան է տալիս այն դեպքում, երբ արգումենտում տրված սիմվոլը տառ է կամ թվանշան։
func isLetterOrDigit(r rune) bool { return unicode.IsLetter(r) || unicode.IsDigit(r) }
Լեքսիկական անալիզատորի Next
մեթոդում ավելացնենք մի բլոկ, որը կարդում է տառով սկսվող տառաթվային հաջորդականություն, որոնում է այն ծառայողական բառերի աղյուսակում և, եթե գտնվել է, ապա վերադարձնում է համապատասխան թոքենը, հակառակ դեպքում վերադարձնում է Ident
թոքենը։
... if unicode.IsLetter(ch) { iden := s.scanWith(isLetterOrDigit) tok, iskey := keywords[iden] if !iskey { tok = Ident } return iden, tok } ...
Փոփոխություններ քերականական անալիզատորում
Նախ factor
մեթոդում ավելացնենք մի ճյուղ, որը վերլուծում է փոփոխականի առկայությունը.
... if p.look == scanner.Ident { nam := p.lexeme p.match(scanner.Ident) return asteval.Variable(nam) } ...
Քերականական անալիզատորում ավելացնենք երեք նոր մեթոդ՝ քերականության հետևյալ երեք կանոնների համար.
Statement = Assignment | Print. Assignment = 'Ident' '=' Expr. Print = 'print' Expr.
Այս պահին մեր հրամանները կարող են սկսվել կա՛մ իդենտիֆիկատորով, կա՛մ print
ծառայողական բառով։ Ես ճյուղավորման համար ընտրել եմ switch
կառուցվածքը, որպեսզի հետագայում հեշտ լինի նոր հրամանների վերլուծության ճյուղերն ավելացնելը։
func (p *Parser) statement() astexec.Statement { switch p.look { case scanner.Ident: return p.assignment() case scanner.Print: return p.printexpr() } return nil }
Վերագրման հրամանը վերլուծելու համար հերթականությամբ պետք է ճանաչել իդենտիֆիկատորը, հավասարության նշանը և հաջորդող արտահայտությունը։ Ապա կառուցել և վերադարձնել նոր Assignment
հանգույց։
func (p *Parser) assignment() astexec.Statement { nm := p.lexeme p.match(scanner.Ident) p.match(scanner.Equal) ex := p.expr() return astexec.NewAssignment(nm, ex) }
Արտածման հրամանը վերլուծելիս պետք է ճանաչել print
ծառայողական բառը, ապա վերլուծել նրան հաջորդող արտահայտությունը։
func (p *Parser) printexpr() astexec.Statement { p.match(scanner.Print) ex := p.expr() return astexec.NewPrint(ex) }
Քանի որ նոր քերականության մեջ առաջինը Statement
կանոնն է, փոփոխենք Parse
մեթոդն այնպես, որ նախ՝ այն վերադարձնի astexec.Statement
, ապա՝ վերլուծությունը սկսի ոչ թե expr
մեթոդից, այլ statement
մեթոդից։
func (p* Parser) Parse(src string) astexec.Statement { p.look = scanner.None p.scan = scanner.New(src) p.match(scanner.None) return p.statement() }
Երկխոսության ցիկլը
Ձևափոխված երկխոսության ցիկլում ստեղծում ենք նոր Environment
օբյեկտ, որում պահվելու են փոփոխականների արժեքները։ Երբ Parse
մեթոդը վերադարձնում է հրամանին համապատասխան աբստրակտ քերականական ծառը, կանչում ենք նրա Execute
մեթոդը՝ արգումենտում տալով env
օբյեկտը որպես կատարման միջավայր։
func Run() { reader := bufio.NewReader(os.Stdin) parser := new(parser.Parser) env := environ.New() for { fmt.Printf("> ") sr, _ := reader.ReadString('\n') ex := string(sr) if '.' == ex[0] { break } ast := parser.Parse(ex) ast.Execute(env) } }
Հաշվարկիչից դեպի լեզվի ինտերպրետատոր
Հաշվարկիչի այս հերթական զարգացումը նպատակ ունի առաջին քայլն անել պարզագույն REPL ցիկլից դեպի ծրագրավորման լեզվի ինտերպրետատոր։ Նախ՝ լեքսիկական անալիզատորը կփոփոխվի այնպես, որ հրամանային տողի փոխարեն ծրագրի տեքստը ընթերցվի ֆայլից։ Ապա՝ լեզվի հրամանների համակարգը կընդլայնվի ներածման և հաջորդման հրամաններով։
Ծրագրի կոդի ընթերցումը ֆայլից
Ձևափոխենք լեքսիկական անալիզատորն այնպես, որ ծրագրի նիշերն ընթերցվեն ոչ թե տողից, այլ bufio.Reader
օբյեկտից։ Scanner
կառուցվածքի նոր տեսքն ահա այսպիսինն է.
type Scanner struct { source *bufio.Reader line int }
Որտեղ line
փոփոխականը նախատեսված է տողի համարը պահելու համար։ Այն մեզ հարկավոր է լինելու քերականական սխալների տեղը նշելիս։
Փոփոխության է ենթարկվում նաև Scanner
-ի կոնստրուկտորը.
func New(src *bufio.Reader) *Scanner { return &Scanner{src, 1} }
Հոսքից նիշեր կարդալիս անհրաժեշտ է մի ֆունկցիա, որը վերադարձնում է հերթական նիշը, բայց այն չի հեռացնում հոսքից։ Եթե հոսքն արդեն դատարկ է, ապա վերադարձնում է 0
։
func (s *Scanner) peek() rune { br, ok := s.source.Peek(1) if ok != nil { return 0 } return rune(br[0]) }
Մեկ այլ ֆունկցիա կարդում և վերադարձնում է հերթական սիմվոլը։ Այս դեպքում նույնպես, եթե հոսքն արդեն դատարկ է, ապա վերադարձնում է 0
։
func (s *Scanner) char() rune { ch, _, ok := s.source.ReadRune() if ok != nil { return 0 } return ch }
scanWith
մեթոդն արդեն ձևափոխված է այնպես, որ օգտագործվի char
մեթոդը։ Այստեղ ձևավորվում է նաև line
դաշտի ընթացիկ արժեքը։
func (s* Scanner) scanWith(predicate func(r rune) bool) string { res := "" ch := s.char() for predicate(ch) { res += string(ch) if ch == '\n' { s.line++ } ch = s.char() } s.source.UnreadRune() return res }
Փոքր փոփոխություններ է կրել նաև քերականական անալիզատորը. Parse
մեթոդը արգումենտում ստանում է ոչ թե տող, այլ bufio.Reader
օբյեկտի ցուցիչ։
func (p* Parser) Parse(src *bufio.Reader) astexec.Statement { p.look = scanner.None p.scan = scanner.New(src) p.match(scanner.None) return p.sequence() }
Այս փոփոխությունները բավական են, որպեսզի ֆայլի դեսկրիպտորից ստեղծվի bufio.Reader
օբյեկտ և նրանից ընթերցվի ծրագիրը։
Լեզվի քերականության ընդլայնում
Հաշվարկիչի լեզուն ընդլայնված է երկու նոր հրամաններով՝ տվյալների ներածման և հրամաններ հաջորդման։
Sequence = Statement {';' Statement}. Input = 'input' 'Ident'.
Հրամանների հաջորդում
Հաջորդման հրաման (կամ հրամանների հաջորդման կառուցվածք) ասելով կհասկանանք ծրագրի տեքստում իրար հետևից գրված հրամանները, որոնք բաժանված են “;
” նիշով։ Պետք է ուշադրություն դարձնել, որ ոչ թե ամեն մի հրաման ավարտվում է “;
” նիշով, այլ նրանով հրամաններն իրարից բաժանվում են։
Sequence
կառուցվածքը պարունակում է միակ children
դաշտը, որը պարունակում է հաջորդականություն կազմող հրամանների ցուցիչները։
type Sequence struct { children *list.List }
Կոնստրուկտորը պարզապես արժեքավորում է children
դաշտը։
func NewSequence() *Sequence { return &Sequence{children: list.New().Init()} }
Append
մեթոդը ցուցակում ավելացնում է հրամանների հաջորդականության հերթական տարրը։
func (q *Sequence) Append(st Statement) { q.children.PushBack(st) }
Կատարման ժամանակ պարզապես հաջորդաբար կատարվում են children
ցուցակի հրամանները (ճիշտ կլիներ, իհարկե, այստեղ ստուգել, որ ցուցակի տարրը nil
չլինի)։
func (q *Sequence) Execute(env *environ.Environment) { for e := q.children.Front(); e != nil; e = e.Next() { st := e.Value.(Statement) st.Execute(env) } }
Լեքսիկական անալիզատորի թոքենների ցուցակում ավելացնենք Semic
թոքենը՝ “;
” նիշի համար։
Քերականական անալիզատորում ավելացնենք sequence
մեթոդը։ Այն ստեղծում է նոր Sequence
օբյեկտ, ապա նրանում ավելացնում է հերծական վերլուծած հրամանները։
func (p *Parser) sequence() astexec.Statement { seq := astexec.NewSequence() st := p.statement() seq.Append(st) for p.look == scanner.Semic { p.match(scanner.Semic) seq.Append(p.statement()) } return seq }
Եվ նշենք, որ Parse
մեթոդը ձևափոխված է այնպես, որ լեզվի վերլուծությունը սկսվի հենց sequence
մեթոդից։
Ներածման հրաման
Ներածման հրամանը որոշվում է input
ծառայողական բառով և նրան հետևող իդենտիֆիկատորով։ Այն օգտագործողից պահանջում է ստեղնաշարից ներմուծել ամբողջ թվի արժեքը։
Աբստրակտ քերականական ծառում ներածման հրամանի հանգույցն ունի հետևյալ տեսքը, որտեղ name
դաշտը այն փոփոխականի անունն է, որի համար պետք է կարդալ արժեքը։
type Input struct { name string }
Կոնստրուկտորը պարզ է.
func NewInput(nm string) *Input { return &Input{name: nm} }
Ներածման հրամանը կատարելու համար ստեղծում ենք bufio.Reader
օբյեկտ՝ os.Stdin
ֆայլի հետ կապված։ Ապա կարդում ենք տող, նրանով արժեքավորում նոր big.Int
օբյեկտ և փոփոխականի անունի հետ միասին ավելացում միջավայրում։
func (i *Input) Execute(env *environ.Environment) { reader := bufio.NewReader(os.Stdin) num, _ := reader.ReadString('\n') value := new(big.Int) value.SetString(num, 10) env.Set(i.name, value) }
Parser
կառուցվածքի inputval
մեթոդը նախ չանաչում է scanner.Input
թոքենը, ապա իդենտիֆիկատորը, վերջինիս համապատասխան լեքսեմն էլ օգտագործելով ստեղծում է նոր astexec.Input
օբյեկտ։
func (p *Parser) inputval() astexec.Statement { p.match(scanner.Input) nm := p.lexeme p.match(scanner.Ident) return astexec.NewInput(nm) }
Եվ ներածման հրամանը վերլուծության ենթական հրամանների շարքում ավելացնելու համար statement
մեթոդում ավելացնում ենք նոր ճյուղ։
func (p *Parser) statement() astexec.Statement { switch p.look { case scanner.Ident: return p.assignment() case scanner.Print: return p.printexpr() case scanner.Input: return p.inputval() } return nil }
Ծրագրի մուտքի կետը
main
փաթեթի main
ֆունկցիայում նախ ստուգում ենք, որ հրամանայի տողում տրված արումենտները լինեն ճիշտ երկու հատ՝ len(os.Args) != 2
։ Ապա փորձում ենք os.Open
ֆունկցիայով բացել ծրագրի ֆայլը՝ անհաջողության դեպքում տալով հաղորդագրություն։
func main() { if len(os.Args) != 2 { fmt.Println("Source file missing.") os.Exit(1) } fin, err := os.Open(os.Args[1]) if err != nil { fmt.Println("Cannot open input file.") os.Exit(2) } defer fin.Close() par := new(parser.Parser) ast := par.Parse(bufio.NewReader(fin)) env := environ.New() ast.Execute(env) }
Օրինակ
Ստեղծենք test0.calc
անունով ֆայլ և նրա մեջ գրենք հետևյալը.
input a; input b; c = a+b; print c.
Համեմատում, ճյուղավորում, կրկնություն
Ինտերպրետատորի զարգացման այս քայլում ես պլանավորել էի իրականացնել ճյուղավորման և կրկնման հրամանները։ Բայց, քանի որ համեմատման գործողությունները սերտորեն կապված են դրանց հետ, ես նախ ընդլայնեցի արտահայտություններն այնպես, որ նրանք ընդգրկեն համեմատման վեց գործողություններ, ապա նոր միայն ընդլայնեցի հրամանների համակարգը ճյուղավորման և կրկնման հրամաններով։
Համեմատման գործողություններ
Երկու թվերի համեմատման գործողությունների համար ես ընտրել եմ հետևյալ նշանակումները․
# =
- հավասար է, # <>
- հավասար չէ, # >
- մեծ է, # >=
- մեծ է կամ հավասար, # <
- փոքր է, # <=
- փոքր է կամ հավասար:
Արտահայտությունների քերականությունն ընդլայնվում է և ստանում է այսպիսի տեսք․
Relation = Expr [RelOper Expr]. RelOper = '=' | '<>' | '>' | '>=' | '<' | '<='. Expr = Term {('+' | '-') Term}. Term = Factor {('*' | '/') Factor}. Factor = 'Number' | 'Ident' | '-' Factor | '(' Expr ')'.
Լեքսիկական անալիզատորի փոփոխությունները
Թոքենների ցուցակում ավելացել են հաստատուններ վերը նշված համեմատման գործողությունների համար․
const ( None byte = iota ... Equal // '=' NotEq // '<>' Great // '>' GrEq // '>=' Less // '<' LsEq // '<=' ... )
Քանի որ սրանով թվաբանական ու համեմատման գործողությունների քանակն իրար հետ վերցրած հասավ տասի, ես որոշեցի լեքսեմներին թոքեններ համապատասխանեցնելու համար սահմանել մի հեշ աղյուսակ․
var operations = map[string]byte{ "+": Add, "-": Sub, "*": Mul, "/": Div, "=": Equal, "<>": NotEq, ">": Great, ">=": GrEq, "<": Less, "<=": LsEq, }
Իսկ գործողություններ կազմող սիմվոլները ճանաչելու համար սահմանեցի isOpChar
ֆունկցիան, որպեսզի հնարավորություն ունենամ գործողությունների նշաններ կարդալ scanWith
մեթոդով․
func isOpChar(r rune) bool { return strings.ContainsRune("+-*/=><", r) }
Համապատախանաբար Next
մեթոդի մարմնում ավելացավ մի նոր ճյուղ, որը կարդում և ճանաչում թվաբանական ու համեմատման գործողությունների նշանները․
func (s *Scanner) Next() (string, byte) { ... // operations if isOpChar(ch) { str := s.scanWith(isOpChar) tok, iskey := operations[str] if !iskey { tok = None } return str, tok } ... }
Աբստրակտ քերականական ծառի փոփոխությունները
Քանի որ համեմատումները բինար գործողություններ են, նրանց համապատասխան ծառի հանգույցներ կստեղծենք NewBinary
կոնստրուկտորով։ Փոփոխություններ կատարվել են միայն Evaluate
մեթոդում։ big
փաթեթի Int
կառուցվածքի համար սահմանված Cmp
մեթոդը համեմատում է երկու Big
թվեր և վերադարձնում է -1, 0, 1, երբ առաջին թիվը համապատասխանաբար փոքր է, հավասար է կամ մեծ է երկրորդ թվից։
func (e *Expression) Evaluate(env *environ.Environment) *big.Int { var res *big.Int = new(big.Int) switch e.class { ... case binary: exo := e.first.Evaluate(env) exi := e.second.Evaluate(env) switch e.operation { ... case "=": u := exo.Cmp(exi) res.SetInt64(toInt64(u == 0)) case "<>": u := exo.Cmp(exi) res.SetInt64(toInt64(u != 0)) case ">": u := exo.Cmp(exi) res.SetInt64(toInt64(u > 0)) case ">=": u := exo.Cmp(exi) res.SetInt64(toInt64(u >= 0)) case "<": u := exo.Cmp(exi) res.SetInt64(toInt64(u < 0)) case "<=": u := exo.Cmp(exi) res.SetInt64(toInt64(u <= 0)) } } return res }
Քերականական անալիզատորի փոփոխությունները
Քերականական անալիզատորում ավելացել է relation
մեթոդը, որը տառացիորեն համընկնում է քերականության հետևյալ կանոնին.
Relation = Expr [RelOper Expr]. RelOper = '=' | '<>' | '>' | '>=' | '<' | '<='.
func (p *Parser) relation() *asteval.Expression { ex := p.expr() if p.look >= scanner.Equal && p.look <= scanner.LsEq { op := p.lexeme p.match(p.look) ex = asteval.Binary(op, ex, p.expr()) } return ex }
Ճյուղավորման և կրկնման կառուցվածքներ
Երկու նոր հրամանները, որոնք անհրաժեշտ են քիչ թե շատ կարգին ալգորիթմներ գրելու համար դրանք ճյուղավորման ու կրկնման հրամաններն են։ Քերականական տեսակետից նրանք ունեն այսպիսի տեսք.
Statement = ... | Branching | Looping. Branching = 'if' Relation 'then' Sequence {'elseif' Relation 'then' Sequence} ['else' Sequence] 'end'. Looping = 'while' Relation 'do' Sequence 'end'.
Լեքսիկական անալիզատորի փոփոխությունները
Թոքենների ցուցակում պետք է հաստատուններ ավելացնել if
, then
, elseif
, else
, while
, do
և end
ծառայողական բառերի համար։
const ( None byte = iota ... If // 'if' Then // 'then' ElseIf // 'elseif' Else // 'else' While // 'while' Do // 'do' End // 'end' ... )
Ծառայողական բառերը թոքենների արտապատկերող հեշ աղյուսակը նույնպես պետք է ընդլայնել հետևյալ կերպ.
var keywords = map[string]byte{ ... "if": If, "then": Then, "elseif": ElseIf, "else": Else, "while": While, "do": Do, "end": End, }
Աբստրակտ քերականական ծառի ընդլայոնւմը
astexec
փաթեթում ավելանում են Branching
և Looping
կառուցվածքներն իրենց կոնստրուկտորներով ու Execute
մեթոդներով։
Branching
կառուցվածքը, որ նախատեսված է ճյուղավորման հրամանի համար, պարունակում է cond
դաշտը որպես պայմանի արտահայտություն, thenpart
դաշտը որպես հրամանների հաջորդականություն, որոնք պիտի կատարվեն այն դեպքում, երբ cond
արտահայտության արժեքը հաշվարկվել է զրոյից տարբեր, և elsepart
դաշտը որպես հարմանների հաջորդականություն, որոնք կատարվում են cond
արտահայտության զրո հաշվարկված արժեքի դեպքում։
type Branching struct { cond *asteval.Expression thenpart, elsepart Statement }
Կոնստրուկտորը ստանում է միայն երկու արգումենտ՝ cond
և thenpart
դաշտերի համար, իսկ elsepart
դաշտն արժեքավորվում է առանձին SetElse
մեթոդով։
func NewBranching(cd *asteval.Expression, tp Statement) *Branching { return &Branching{cond: cd, thenpart: tp, elsepart: nil} } func (b *Branching) SetElse(e Statement) { b.elsepart = e }
Ճյուղավորման հրամանի կատարումը շատ պարզ է։ Նախ հաշվարկվում է cond
արտահայտության արժեքը, ապա, եթե այն տարբեր է զրոյից՝ կատարվում է thenpart
հրամանների հաջորդականությունը, հակառակ դեպքում՝ elsepart
հաջորդականությունը։
func (b *Branching) Execute(env *environ.Environment) { ex := b.cond.Evaluate(env) if 0 != ex.Cmp(big.NewInt(0)) { b.thenpart.Execute(env) } else { b.elsepart.Execute(env) } }
Կրկնման հրամանը մոդելավորող Looping
կառուցվածքում cond
դաշտը ցիկլի կատարման պայմանն է, իսկ body
դաշտը՝ ցիկլի մարմինը։ Կոնստրուկտորը պարզապես արժեքավորում է այդ դաշտերը։
type Looping struct { cond *asteval.Expression body Statement } func NewLooping(cd *asteval.Expression, bd Statement) *Looping { return &Looping{cd, bd} }
Կրկնման հրամանը կատարելու համար նախ հաշվվում է cond
արտահայտությունը։ Եթե նրա արժեքը զրոյից տարբեր է, ապա կատարվում է body
հրամանների հաջորդականությունը։ Նույնը կրկնվում է այնքան ժամանակ, քանի դեռ cond
արտահայտության արժեքը տարբեր է զրոյից։
func (w *Looping) Execute(env *environ.Environment) { zr := big.NewInt(0) ex := w.cond.Evaluate(env) for 0 != ex.Cmp(zr) { w.body.Execute(env) ex = w.cond.Evaluate(env) } }
Քերականական անալիզատորի ընդլայնումը
Ճյուղավորման հրամանի քերականական կանոնը պահանջում է, որ հրամանը սկսվի if
ծառայողական բառով, որին հետևում է պայմանը ներկայացնող արտահայտությունը, ապա then
ծառայողական բառը, որին հետևում են այն հրամանները, որոնք կատարվելու են, երբ պայմանի արտահայտության արժեքը հաշվարկվի զրոյից տարբեր։
func (p *Parser) branching() astexec.Statement { p.match(scanner.If) ex := p.relation() p.match(scanner.Then) st := p.sequence() res := astexec.NewBranching(ex, st)
Ճյուղավորման հրամանի առաջին պայմանին կարող են հաջորդել անսահմանափակ քանակով elseif
կառուցվածքներ՝ իրենց համապատասխան հրամանների հաջորդականություններով։
t := res for p.look == scanner.ElseIf { p.match(scanner.ElseIf) ex = p.relation() p.match(scanner.Then) st = p.sequence() v := astexec.NewBranching(ex, st) t.SetElse(v) t = v }
Եվ հրամանը կարող է ունենալ միակ else
ճյուղ, որում թվարկված հրամանները կկատարվեն, եթե մինչև իրեն նշված բոլոր պայմանների արտահայտությունների հաշվարկը վերադարձրել զրոյական արժեք։
if p.look == scanner.Else { p.match(scanner.Else) se := p.sequence() t.SetElse(se) }
Ճյուղավորման հրամանն ավարտվում է end
ծառայողական բառով։
p.match(scanner.End) return res }
Կրկնման հրամանը սկսվում է while
ծառայողական բառով, որին հետևում են կրկնման պայմանի արտահայտությունը, do
բառը, հրամանի մարմնի հրամանների հաջորդականությունը և ապա end
բառը։
func (p *Parser) looping() astexec.Statement { p.match(scanner.While) ex := p.relation() p.match(scanner.Do) st := p.sequence() p.match(scanner.End) return astexec.NewLooping(ex, st) }
Բնականաբար branching
և looping
մեթոդներն իրականացնելուց հետո պետք է statement
մեթոդում ավելացնել նրանց համապատասխան ճյուղերը։
Օրինակներ
Հետևյալ երկու օրինակները ցուցադրում են ճյուղավորման ու կրկնման հրամանների օգտագործումը։
Առաջին օգտագործողից հարցնում է ինչ-որ թիվ և ամեն մի թվի համար արտածում է պատասխան։
input x; if x = 1 then print 100 elseif x = 2 then print 200 elseif x = 3 then print 300 elseif x = 4 then print 400 else print 100000 end.
Երկրորդ օրինակը հաշվում է 10-ի ֆակտորիալը։
i = 10; r = 1; while i do print i; r = r * i; i = i - 1 end; print r.
No comments:
Post a Comment