«Հաշվարկիչ կամ արտահայտությունների ինտերպրետատոր» հոդվածով ես սկսեցի պատմել, թե ինչպես կարելի է գրել մեծ ամբողջ թվերի հետ թվաբանական գործողություններ կատարող մի պարզ հաշվարկիչ՝ կալկուլյատոր։ Այդ հաշվարկիչը օգտագործողի հետ շփվում է երկխոսության ռեժիմում՝ հերթականությամբ հարցնելով արտահայտության տեքստը, հաշվարկելով այն և արտածելով հաշվարկման արդյունքները։
Երկրորդ հոդվածում, որ կոչվում է «Հաշվարկիչ վերագրման և արտածման հրամաններով», ես ընդլայնեցի հաշվարկիչն այնպես, որ այն հնարավորություն ունենա փոփոխականների մեջ պահել արտահայտության արժեքը, իսկ արտածման հրամանով արտածի այն։ Կարելի է ասել, որ պարզ հաշվարկիչը վերածվեց ամենապարզ ծրագրավորման լեզվի՝ վերագրման ու արտածման հրամաններով և թվաբանական արտահայտություններով։ Այս երկրորդ հաշվարկիչը նույնպես աշխատում է երկխոսության ռեժիմում։
Հաշվարկիչի այս հերթական զարգացումը նպատակ ունի առաջին քայլն անել REPL ցիկլից դեպի ծրագրավորման լեզվի ինտերպրետատոր։ Նախ՝ լեքսիկական անալիզատորը կփոփոխվի այնպես, որ հրամանային տողի փոխարեն ծրագրի տեքստը ընթերցվի ֆայլից։ Ապա՝ լեզվի հրամանների համակարգը կընդլայնվի ներածման և հաջորդման հրամաններով։
Ծրագրի կոդի ընթերցումը ֆայլից
Ձևափոխենք լեքսիկական անալիզատորն այնպես, որ ծրագրի նիշերն ընթերցվեն ոչ թե տողից, այլ bufio.Reader
օբյեկտից։ Scanner
կառուցվածքի նոր տեսքն ահա այսպիսինն է.
type Scanner struct { source *bufio.Reader line int }
Որտեղ line
փոփոխականը նախատեսված է տողի համարը պահելու համար։ Հետագայում այն մեզ հարկավոր է լինելու քերականական սխալների տեղը նշելիս։
Փոփոխության է ենթարկվում նաև Scanner
-ի կոնստրուկտորը։ Տողի փոխարեն այն այժմ ստանում է bufio.Reader
կառուցվածքի ցուցիչ։
func New(src *bufio.Reader) *Scanner { return &Scanner{src, 1} }
Հոսքից նիշեր կարդալիս անհրաժեշտ է մի ֆունկցիա, որը վերադարձնում է հերթական նիշը, բայց այն չի հեռացնում հոսքից։ Եթե հոսքն արդեն դատարկ է, ապա վերադարձնում է 0։
func (s *Scanner) peek() rune { br, err := s.source.Peek(1) if err != nil { return 0 } return rune(br[0]) }
Մեկ այլ ֆունկցիա կարդում և վերադարձնում է հերթական սիմվոլը։ Այս դեպքում նույնպես, եթե հոսքն արդեն դատարկ է, ապա վերադարձնում է 0։
func (s *Scanner) char() rune { ch, _, err := s.source.ReadRune() if err != 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 { ... }
Այս փոփոխությունները բավական են, որպեսզի ֆայլի դեսկրիպտորից ստեղծվի bufio.Reader
օբյեկտ և նրանից ընթերցվի ծրագիրը։
Լեզվի քերականության ընդլայնում
Հաշվարկիչի լեզուն ընդլայնված է երկու նոր հրամաններով՝ տվյալների ներածման և հրամաններ հաջորդման։
Sequence = Statement {';' Statement}. Input = 'input' 'Ident'.
Հրամանների հաջորդում
Հաջորդման հրաման (կամ հրամանների հաջորդման կառուցվածք) ասելով կհասկանանք ծրագրի տեքստում իրար հետևից գրված հրամանները, որոնք բաժանված են “;” նիշով։ (Պետք է ուշադրություն դարձնել, որ ոչ թե ամեն մի հրաման ավարտվում է “;” նիշով, այլ նրանով հրամաններն իրարից բաժանվում են։)
Sequence
կառուցվածքը պարունակում է միակ children
դաշտը, որը պարունակում է հաջորդականություն կազմող հրամանների ցուցիչները։
type Sequence struct { children *list.List }
Կոնստրուկտորը children
դաշտն արժեքավորում է նոր ստեղծված list.List
օբյեկտով։
func NewSequence() *Sequence { return &Sequence{children: list.New().Init()} }
Քանի որ հրամանները քերականական անալիզատորի կողմից վերլուծվելու են հաջորդաբար (ամեն մի հրամանը վերլուծելիս դեռ հայտնի չէ, թե ևս քանի հրամաններ են հետևելու նրան), նախատեսված է Append
մեթոդը, որը Sequence
օբյեկտի children
ցուցակում ավելացնում է հերթական տարրը։
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
մեթոդից։
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() }
Ներածման հրաման
Ներածման հրամանը որոշվում է 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
Սկզբնաղբյուրը։ http://code.google.com/p/calculator-with-big-integers/wiki/CalculatorII
Ծրագրի կոդը։ http://calculator-with-big-integers.googlecode.com/svn/tags/calc-ii/calcgo/
Comments: no replies