Սկիզբ » Ուսումնական նյութեր » Ծրագրավորում » Ծրագրավորման լեզուներ » Go » Հաշվարկիչից դեպի լեզվի ինտերպրետատոր

Հաշվարկիչից դեպի լեզվի ինտերպրետատոր

| Դեկտեմբեր 8, 2012 | Մեկնաբանված չէ |

«Հաշվարկիչ կամ արտահայտությունների ինտերպրետատոր» հոդվածով ես սկսեցի պատմել, թե ինչպես կարելի է գրել մեծ ամբողջ թվերի հետ թվաբանական գործողություններ կատարող մի պարզ հաշվարկիչ՝ կալկուլյատոր։ Այդ հաշվարկիչը օգտագործողի հետ շփվում է երկխոսության ռեժիմում՝ հերթականությամբ հարցնելով արտահայտության տեքստը, հաշվարկելով այն և արտածելով հաշվարկման արդյունքները։

Երկրորդ հոդվածում, որ կոչվում է «Հաշվարկիչ վերագրման և արտածման հրամաններով», ես ընդլայնեցի հաշվարկիչն այնպես, որ այն հնարավորություն ունենա փոփոխականների մեջ պահել արտահայտության արժեքը, իսկ արտածման հրամանով արտածի այն։ Կարելի է ասել, որ պարզ հաշվարկիչը վերածվեց ամենապարզ ծրագրավորման լեզվի՝ վերագրման ու արտածման հրամաններով և թվաբանական արտահայտություններով։ Այս երկրորդ հաշվարկիչը նույնպես աշխատում է երկխոսության ռեժիմում։

Հաշվարկիչի այս հերթական զարգացումը նպատակ ունի առաջին քայլն անել 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/

Հաշվարկիչից դեպի լեզվի ինտերպրետատոր, 9.8 out of 10 based on 10 ratings

Բաժին: Go, Ծրագրավորման լեզուներ, Ծրագրավորում, Կոմպիլյատորներ, Ուսումնական նյութեր

Կիսվել , տարածել , պահպանել

VN:F [1.9.20_1166]
Rating: 9.8/10 (10 votes cast)

Մեկնաբանեք

Կհաստատվեն միայն մեսրոպատառ հայերենով գրած մեկնաբանությունները

246