Պատկերացենք մի վերացական համակարգիչ, որի պրոցեսորի հրամանները նախատեսված են ստեկային վարքով աշխատելու համար։ Օրինակ ADD հրամանը արգումենտներ չունի, այլ վերցնում է ստեկի գագաթի երկու տարրերը, դրանք գումարում է իրար և արդյունքը նորից գրում է ստեկում։ Ենթադրենք, թե մեքենան կարող է աշխատել միայն ամբողջ թվերի հետ։
Ֆիզիկական կառուցվածքի տեսակետից այդ մեքենան (ստեկային համակարգիչը) ունի ընդհանուր օգտագործման հիշողություն, որում պահվում են կատարվող ծրագիրը, ստատիկ տվյալները, գլոբալ փոփոխականներ, և հենց այդ հիշողության ինչ-որ տիրույթում մոդելավորվում է ստեկը։
(defconstant +size+1024"Հիշողության չափը")(defparameter *memory*(make-array +size+)"Հիշողությունը որպես տրված չափի զանգված")
Մեքենան ունի նաև երեք ռեգիստրներ. ա) հերթական հատարվող հրամանի ցուցիչ՝ IP, բ) ստեկի գագաթի ցուցիչ՝ SP և գ) ստեկի կադրի ցուցիչ՝ FP, որը ֆունկցիաների կանչի ժամանակ օգտագործվում է ֆունկցիայի արգումենտներին և լոկալ փոփոխականների հասցեներին դիմելու համար։
Պարզության համար ընդունենք, որ կատարվող ծրագիրը բեռնվում է հիշողության 0 հասցեից։ Ծրագիր զբաղեցրած տիրույթին հաջորդում է ստատիկ տվյալների և գլոբալ փոփոխականների տիրույթը՝ տվյալների սեգմենտը։ Իսկ տվյալների սեգմենտին հաջորդում է ստեկի սեգմենտը։ Օրինակ, եթե հիշողության մեջ բեռնվել է 16 հրամաններից բաղկացած ծրագիր, որում կան 8 գլոբալ փոփոխականներ, ապա կոդի սեգմենտը կսկսվի 0 հասցեից, տվյալների սեգմենտը՝ 16 հասցեից, իսկ ստեկի սեգմենտը՝ 24 հասցեից։
«Ստեկային մեքենայի» հրամանները ստորև կներկայացվեն նրա «ասեմբլերի» լեզվով։ Մեքենայի հիշողություն բեռնվելուց հետո, բնականաբար, հրամանները կունենան այլ տեսք։
Ստեկում հաստատուն թիվ կարելի է ավելացնել PUSH հրամանով։ Օրինակ,
PUSH 777;ստեկումավելացնել777արժեքը
Որևէ փոփոխականի արժեք ստեկում ավելացնելու համար նույնպես օգտագործվում է PUSH հրամանը՝ արգումենտում ունենալով փոփոխականի իդենտիֆիկատորը։ Փոփոխականների անունները կարող են սկսվել լատինական փոքրատառով և պարունակել միայն փոքրատառեր և թվանշաններ։ Օրինակ,
PUSH a ;ստեկումավելացնել a փոփոխականիզբաղեցրածհասցեումգրվածարժեքը
PUSH wu3 ;ստեկումավելացնել wu3 փոփոխականիզբաղեցրածհասցեումգրվածարժեքը
Եթե հարկավոր է ստեկում ավելացնել հենց ռեգիստրի պարունակությունը, ապա ստեկի անունը պետք է սկսել «%» նիշով։ ենթադրենք M-ը ստեկային մեքենայի հիշողությունն է։ Այս դեպքում․
Ստեկից տվյալներ հեռացնելու համար է նախատեսված POP հրամանը։ Ստեկի գագաթի տարրը որևէ փոփոխականի վերագրելու համար POP հրամանն արգումենտում ունիփոփոխականի անունը։ Օրինակ.
POP u
Ինչպես PUSH հրամանը, նույնպես էլ POP հրամանը կարող է օգտագործել ռեգիստրի արժեքը՝ անուղակի հասցեավորում կազմակերպելու համար։ Օրինակ.
POP FP+1
POP FP
POP FP-12
Նշված երեք տիպի POP հրամաններն էլ իրականացված են հետևյալ ֆունկցիաներով։
Օգտագործողի կողմից տվյալները ներմուծվում են IN հրամանով, որը ներմուծած թիվը գրում է ստեկի գագաթում։ Իսկ գործողությունների արդյունքն օգտագործողի համար արտածվում են OUT հրամանով, որը հեռացնում է ստեկի գագթի արժեքը և արտածում է ստանդարտ արտածման հոսքին։ Հետևյալ երկու ֆունկցիաներն այդ հրամանների իրականացումներն են.
(defun input-num ()(format t "? ")(push-value(parse-integer (read-line))))(defun output-num ()(format t "> ~d~%"(pop-value)))
Ստեկային մեքենայի հրամանների համակարգում նախատեսված են նաև թվաբանական ու համեմատման գործողություններ, ինչպիսիք են գումարում, հանում, հավասարության ստուգում և այլն։ Բայց նախ սահմանենք binary-op և unary-op ֆունկցիաները, որոնց օգնությամբ կսահմանենք կոնկրետ բինար և ունար գործողություններ։
Մինչև այս պահը թվարկված հրամաններով կարելի է կառուցել միայն գծային ալգորիթմներ։ Որպեսզի հնարավոր լինի ծրագրավորել ճյուղավորվող ու կրկնվող ալգորիթմներ, մեքենան պետք է ունենա անցման հրամաններ։ Ստեկային մեքենայի մոդելում JZ հրամանը անցում է կատարում տրված հասցեով հրամանին, եթե ստեկի գագաթի տարրը զրո է։ Նմանապես JNZ հրամանը անցում է կատարում տրված հասցեով հրամանին, եթե ստեկի գագաթում զրոյից տարբեր արժեք է։ Այս երկու հրամաններն էլ հեռացնում են ստեկի գագաթի տարրը։ Իսկ JUMP հրամանը պարզապես առանց պայմանի անցում է։ Բոլոր երեք հրամաններն էլ փոխում են IP ռեգիստրի արժեքը։
Ասեմբլերային ծրագրի տեքստում նշիչներ (անցման կետեր) սահմանելու համար է LABEL փսևդոհրամանը։ Այն կատարվող հրամանի չի թարգմանվում, պարզապես իր արգումենտում տրված իդենտիֆիկատորին կապում է IP ռեգիստրի ընթացիկ արժեքը՝ հետագա հղումների համար։
LABEL wb0
;...
JUMP wb0
Եվս երկու հրամաններ, որոնք ապահովելու են ֆունկցիայի կանչ և վերադարձ ֆունկցիայից։ CALL հրամանի արգումենտը LABEL փսևդոհրամանով սահմանված իդենտիֆիկատոր է։
(defun call-func (address)(push-value0);ֆունկցիայիվերադարձրածարժեքիտեղը(push-value*ip*);վերադարձիհասցեն, IP ռեգիստրիարժեքը(push-value*fp*);ստեկիկադրիհինարժեքը(setf *fp**sp*);ստեկիկադրիընթացիկարժեքը(setf *ip* address)); IP ռեգիստրինորարժեքը
Ֆունկցիայից վերադարձի համար է նախատեսված RET հրամանը։ Այն տեղափոխում է ստեկի գագաէի տարրը վերադարձվող արժեքի համար նախատեսված տեսը։ վերականգնում է ստեկի ցուցիչի ռգիստրի հին արժեքը և IP ռեգիստրին վերագրում է CALL հրամանի նախատեսած վերադարձի հասցեն։
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Ծրագրավորման լեզուների կոմպիլյացիայի հերցերն ուսումնասիրելիս ես տևական ժամանակ փնտրում էի մի գրադարան, որը հարավորություն կտար առանց մանրամասնությունների մեջ մտնելու գեներացնել Java վիրտուալ մեքենայի class ֆայլեր։ Առաջին որոնումները բերեցին ASM և BCEL գրադարաններին և Jasmin «Java ասեմբլերին»։ Վերջինս հնարավորություն է տալիս մնեմենիկ հրամաններով հրագրավորել ալգորիթմը, ապա այն «ասեմբլացնել» ու ստանալ վիրտուալ մեքենայի բայթ-կոդ՝ class ֆայլ։
Բայց, տարբեր պատճառներով, այդ գրադարաններից ոչ մեկը ես հարմար չգտա սովորելու համար։ Հետագա որոնումների ընթացքում հանդիպեցի GNU/bytecode գրադարանին, որ Scheme ծրագրավորման լեզվի Kawa իրականացման մաս է կազմում։
Այս գրառման մեջ ես ուզում եմ ֆակտորիալի և ամենամեծ ընդհանուր բաժանարարի հաշվման պարզ օրինակներով ցույց տալ, թե ինչպես կարելի է գեներացնել այդ երկու ալգորիթմները պարունակող class ֆայլը։
Նախ սկսեմ Java տարբերակից.
publicclassAlgorithms{publicstaticint factorial(int n ){if( n ==1)return1;return n * factorial( n -1);}publicstaticint gcd(int n,int m ){while( n != m )if( n > m )
n -= m;else
m -= n;return n;}}
Այս դասի նկարագրությունը Algorithms.java ֆայլի մեջ գրառելուց և javac կոմպիլյատորով թարգմանելուց հետո ստացվում է Algorithms.class ֆայլը։ class ֆայլը կարելի է javap դիզասեմբլերով հետ թարգմանել ու տեսնել թե Java լեզվի կոմպիլյատորն ինչ հրամաններ է գեներացրել։ Այդ բոլոր հրամանների նկարագրությունը կարելի է գտնել Java վիրտուալ մեքենայի նկարագրության մեջ։
Այս արտածումը տրված է JVM-ի հրամաններ մնեմոնիկ ներկայացմամբ, որոնց բացատրությունները բերված են JVM Specification էջում։
Հիմա ցույց տամ, թե ինչպես եմ gnu.bytecode գրադարանի օգտագործմամբ ստեղծում factorial և gcd ալգորիթմները պարունակող class ֆայլը։ Արդեն նշեցի, որ gnu.bytecode գրադարանը Kawa լեզվի իրականացման մաս է։ Չնայած որ կարելի է կոդից առանձնացնել միայն gnu.bytecode-ն և կառուցել առանձին *.jar ֆայլ, ես կօգտագործեմ հենց kawa-1.13.jar ֆայլը։
ՈՒրեմն, նախ ներմուծում եմ gnu.bytecode գրադարանը․ import gnu.bytecode.*;
Հետո ստեղծում եմ ClassType դասի օբյեկտ, որը ներկայացնում է Java վիրտուալ մեքենայի դասը։ Այնուհետև դասի համար որպես ծնող նշում եմ java.lang.Object դասը, իսկ որպես տեսանելիության մոդիֆիկատոր նշում եմ public - Access.PUBLIC հոստատունի օգնությամբ։
// class `Algorithms'ClassType clo =newClassType("Algorithms");
clo.setSuper("java.lang.Object");
clo.setModifiers(Access.PUBLIC);
Քանի որ և՛ factroial, և՛ gcd մեդոդները սահմանելու եմ public ու static մոդիֆիկատորներով, այստեղ սահմանել եմ pubstat հաստատունը․
finalint pubstat =Access.PUBLIC |Access.STATIC;
Հետո clo դասում ավելացնում եմ նոր մեթոդ՝ "factorial" անունով և "(I)I" սիգնատուրայով։ addMethod մեթոդի վերադարձրած հղումև վերագրում եմ Method տիպի mfac փոփոխականին։
Այս երկու հրամանների կատարումից հետո գեներացվում է Algorithms.class ֆայլը։ Կարելի է javap դիզասեմբլերով տեսնել այս ֆայլի պարունակությունը։ Բայց ես կգրեմ մի տեստային ֆայլ, որը կօգտագործի Algwrithms դասի factorial և gcd ֆունկցիաները։
Ինչպե՞ս գրել տողով տրված մաթեմատիկական արտահայտությունը հաշվարկող ծրագիր։ Կարող է թվալ, թե ավելորդ է գրել այս թեմայով, քանի որ կարելի է գտնել բազմաթիվ ու բազմազան գրականություն՝ հարուստ տեսական ու գործնական նյութով։ Բայց ես որոշեցի սկսել մի շարք, որտեղ կպատմեմ, թե ինչպես նախ կառուցել մի քանի թվաբանական գործողություններ կատարող հաշվարկիչ, ապա դրա հենքի վրա՝ քիչ-քիչ ընդլայնելով, ստեղծել ծրագրավորման լեզվի ինտերպրետատոր։
Որպես իրականացման միջոց ընտրել եմ Go լեզուն՝ երկու պատճառով. ա) ես ուզում եմ ավելի խորն ուսումնասիրել այդ լեզուն, և բ) ուզում եմ իմ կուտակած փորձը կիսել նրանց հետ, ովքեր հետաքրքրվում են Go լեզվով։
Ամենից առաջ պետք է որոշել, թե ի՞նչ ֆունկցիոնալությամբ է օժտված լինելու հաշվարկիչը։ Սկզբի համար ես ընտրել եմ թվաբանական չորս բինար գործողություններ՝ գումարում, հանում, բազմապատկում և բաժանում, մեկ ունար գործողություն՝ բացասում, խմբավորման փակագծեր՝ գործողություններ։ Հաշվարկիչն աշխատելու է մեծ (“անսահմանափակ” երկարությամբ) ամբողջ թվերի հետ։ Օգտագործողի տեսակետից հաշվարկիչը ստանալու է արտահայտության տեքստային ներկայացումը, հաշվելու է նրա արժեքը և վերադարձնելու է պատասխանը՝ նորից տեքստային ներկայացմամբ։
EBNF գրառմամբ հաշվարկիչի լեզուն ներկայացնող քերականությունը կունենա այսպիսի տեսք.
Քերականությամբ նկարագրված արտահայտությունների մոդելը աբստրակտ քերականական ծառն է (abstract syntax tree, AST)։ Այդ ծառի հանգույցները ներկայացնում են արտահայտության գործողությունները, իսկ տերևները ներկայացնում են տվյալները։ Օրինակ, 123 + 45 * 6 արտահայտության համար պետք է կառուցել հետևյալ ծառը.
+/ \
/ \
123*/ \
456
Աբստրակտ քերականական ծառը կառուցվում է քերականական անալիզատորի (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 Expressionstruct{classbytevalue*big.Int
operation rune
first, second *Expression}
Ինչպես նշվեց, մենք գործ ենք ունենալու երեք տիպի հանգույցների հետ՝ թիվ, ունար գործողություն և բինար գքործողություն։ Սահմանենք կոնստրուկտոր ֆունկցիաներ նրանց համար։
Թիվ (number) ներկայացնող կոնստրուկտերը արգումենտում ստանում է թվի տեքստային տեսքը, և վերադարձնում է Expression կառուցվածքի նոր ստեղծված նմուշի ցուցիչը, որի class դաշտին վերագրված է number հաստատունը, իսկ value դաշտին՝ big.Int օբյեկտի ցուցիչը։
ՈՒնար գործողությանը համապատասխանող հանգույց ստեղծելու համար պետք է տալ գործողության նիշը և ենթաարտահայտությունը։ Չնայաց, որ մեր դեպքում կա միայն մեկ ունար գործողություն՝ բացասումը, կոնստրուկտոր ֆունկցիան որպես առաջին արգումենտ ընդունում է նաև գործողության նշանը։
func Unary(op rune, ex *Expression)*Expression{return&Expression{class: unary, operation: op, first: ex}}
Բինար գործողության հանգույցի համար էլ պետք է ունենալ գործողության նշանը, աջ ու ձախ ենթաարտահայտությունները։
Արտահայտության արժեքը հաշվելու համար պետք է անցում կատարել նրա համար կառուցած աբստրակտ քերականական ծառով՝ ըստ հետևյալ կանոնների.
եթե 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
Լեքսիկական անալիզատորի խնդիրն է հենց այս տրոհումը։ Քերականական անալիզատորն օգտագործում է թոքենները քերականական կանոնն ընտրելու համար, իսկ լեքսեմներն օգտագործում է ծառի հանգույցների պարունակությունը լրացնելու համար։
Ընթերցվող սիմվոլների տիպը որոշելու համար ներմուծենք Go լեզվի գրադարանային unicode փաթեթը։
import"unicode"
Սահմանենք Scanner կառուցվածքը, որի text դաշտը արտահայտության տեքստի նիշերի զանգվածն է, իսկ pos ինդեքսը ցույց է տալիս հերթական ընթերցվող սիմվոլը։
type Scannerstruct{
text []rune
pos int}
Scanner կառուցվածքի կոնստրուկտորը ստանում է վերլուծության ենթակա արտահայտության տեքստը, նրա վերջից կցում է “;” նիշը որպես արտահայտության ավարտի նշան, ապա տողը վերածում է նիշերի վեկտորի և վերագրում text դաշտին։
Սահմանենք մի օգնական մեթոդ, որը արտահայտության նիշերի շարքից կարդում է տրված պրեդիկատին բավարարող նիշերի անընդհատ շարք և վերադարձնում է դրանցից կազմած տողը։
func (s*Scanner) scanWith(predicate func(r rune)bool)string{
k := s.pos
for predicate(s.text[s.pos]){ s.pos++}returnstring(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 =Addcase'-': res =Subcase'*': res =Mulcase'/': res =Divcase'(': res =LParcase')': res =RParcase';': res =Eos}returnstring(ch), res
}
Սա էլ լեքսիկական անալիզատորի փաթեթեը։ նորից ոչ մի սխալների մշակում չի կարարված զուտ պարզության համար։ Նախագծի հետագա զարգացումներում անպայմանորեն պետք է հաշվի առնել հնարավոր սխալները։
Քերականական անալիզատորն իրականացված է parser փաթեթում։ Այն ըստ քերականական կանոնների վերլուծում է արտահայտության տեքստը և կառուցում է աբստրակտ քերականական ծառը։ Քերականական սխալները չեն մշակվում՝ նորից պարզության համար։
package parser
Ներմուծենք մեր նախապես ստեղծած երկու փաթեթները։
import("scanner""asteval")
Parser կառուցվածքը քերականական անալիզատորի մոդելն է։ Նրա look դաշտը ընթացիկ թոքենն է՝ look-a-head սիմվոլը, lexeme դաշտը ընթացիկ լեքսեմն է, scan դաշտը լեքսիկական անալիզատորի ցուցիչն է։
type Parserstruct{
look byte
lexeme string
scan *scanner.Scanner}
Parser կառուցվածքի համար կոնստրուկտոր չենք գրել, քանի որ դրա կարիքը պարզապես չկա։
Միակ միջոցը, որով քերականական անալիզատորը շփվում է արտաքին աշխարհի հետ, դա Parse մեթոդն է։ Այն ստանում է արտահայտության տեքստը և վերադարձնում է աբստրակտ քերականական ծառի արմատը (եթե վերլուծության ընթացքում սխալներ չեն հայտնաբերվել)։
Parser-ը լեքսիկական անալիզատորից հերթական լեքսեմ-թոքեն զույգը կարդում է match մեթոդի օգնությամբ։ Հետագայում այս մեթոդի մեջ է իրականացվելու որոշ քերականական սխալների մասին ազդարարումը։
ՈՒշադիր նայելով այս արտածման կանոններին տեսնում ենք, որ Factor կանոնը կարելի է կիրառել միայն այն դեպքում, երբ լեքսիկական անալիզատորի look-a-head սիմվոլը “Number”, “-”, “(” թոքեններից որևէ մեկն է։
Եթե look-a-head = “Number”, ապա ստեղծում և վերադարձնում ենք ծառի նոր տերև՝ ընթացիկ լեքսեմի արժեքով։ Եթե look-a-head = “-”, ապա հանդիպել ենք ունար գործողության։ Ռեկուրսիվ կանչով կառուցում ենք գործողության արգումենտի ծառը, ապա ստեղծում ենք ծառի ունար գործողության հանգույց։ Եթե look-a-head = “(”, ապա ռեկուրսիվ կանչով կառուցում ենք փակագծերի ներսում գրված արտահայտության ծառը, ապա match մեթոդի կանչով համոզվում ենք, որ առկա է փակվող փակագիծը։
func (p *Parser) factor()*asteval.Expression{// numberif 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 parenthesesif p.look == scanner.LPar{
p.match(scanner.LPar)
ex := p.expr()
p.match(scanner.RPar)return ex
}returnnil}
Քերականության հաջորդ կանոնը կառուցում է բազմապատկման ու բաժանման բինար գործողություններին համապատասխանող հանգույցները։ Ըստ քերականական երկրորդ կանոնի, Term-ը կարող է լինել կա՛մ Factor, կա՛մ “*” և “/” նիշերով իրար կցված Factor-ների հաջորդականություն․
Term=Factor{("*"|"/")Factor}.
Քերականական անալիզատորի term մեթոդը այս կանոնի տառացի իրականացումն է։
Երրորդ քերականական կանոնը կառուցում է գումարման ու հանման բինար գործողություններին համապատասխանող հանգույցները։ expr մեթոդը շատ նման է term մեթոդին։ Քանի որ մեր քննարկած արտահայտություններում գումարման ու հանման գործողություններն ունեն ամենացածր նախապատվությունը, հենց այս մեթոդն է կանչվում Parse մեթոդից։
Հաշվարկիչի և օգտագործողի շփումն իրականացված է հրամանային տողի օգնությամբ։ Գործարկումից հետո ծրագիրն արտածում է “>” սիմվոլը և սպասում է, որ օգտագործողը ներմուծի արտահայտության տեքստը։ Արտահայտության ներմուծումից և Enter ստեղնի սեղմումից հետո հաշվարկվում և արտածվում է արտահայտությունը արժեքը։
Փաթեթն անվանված է repl՝ (Read-Evaluate-Print-Loop)։
package repl
Ներմուծենք անհրաժեշտ փաթեթները։
import("fmt""os""bufio""parser")
Եվ միակ Run պրոցեդուրան, որը reader-ի միջոցով կարդում է օգտագործողի տված տեքստը, parser-ի միջոցով վերլուծում է այն ու կառուցում է աբստրակտ քերականական ծառ։ Այնուհետև ծառի արմատի համար կանչելով Evaluate մեթոդը՝ հաշվում է արտահայտության արժեքը։ Ցիկլը կատարվում է այնքան ժամանակ, քանի դեռ օգտագործողի ներածած տեքստի առաջին նիշը “.” (կետ) չէ։
Այս անգամ ես ներկայացնում եմ հաշվարկիչի մի նոր զարգացում, որտեղ ավելացված են փոփոխականին արժեքի վերագրման և արտահայտության արժեքի արտածման հրամանները։ Ի տարբերություն միայն թվաբանական գործողություններ կատարող հաշվարկիչի, փոփոխականներով հաշվարկիչը հնարավորություն է տալիս պահպանել և կրկին օգտագործել հաշվարկման արդյունքները։ Օրինակ, կարելի է գրել հետևյալ հրամանները․
a =5
b =4print a +1+ b
որոնց կատարումից հետո հաշվարկիչը կարտածի 10 արդյունքը։
Պետք է նկատի ունենալ, որ վերագրման և արտածման գործողությունները հրամաններ են, այլ ոչ թե արտահայտություններ։
Քանի որ հաշվարկիչը պետք է կարողանա հիշել փոփոխականների արժեքները, սահմանենք Environment կառուցվածքը, որը մասնակցելու է հրամանների կատարման և արտահայտությունների հաշվարկման պրոցեսին և որի միջոցով փոխանցվելու են փոփոխականների արժեքները։
package environ
import"math/big"
Environment կառուցվածքը պարունակում է data դաշտը, որը արտապատկերում է փոփոխականի անունները արժեքներին։ Արտապատկերումը կազմակերպելու համար օգտագործված է Go լեզվի map օբյեկտը։
type Environmentstruct{
data map[string]*big.Int}
Միջավայրի կոնստրուկտորը պարզապես ստեղծում և վերադարձնում է նոր Environment օբյեկտի ցուցիչ։
Քերականության Factor արտածման կանոնում ավելացել է նոր ճյուղ՝ 'Ident', որը ցույց է տալիս, որ փոփոխականները նույնպես արտահայտություն են։ Ընդլայնենք asteval փաթեթի Expression կառուցվածքն այնպես, որ այն սպասարկի նաև փոփոխականները։
type Expressionstruct{classbytevalue*big.Int
varname string
operation rune
first, second *Expression}
Իսկ արտահայտության տիպը որոշող հաստատունների ցուցակում ավելացնենք ևս մեկ հաստատուն՝ variable։
const(
number byte= iota
variable
unary
binary
)
Բնականաբար պետք է ունենալ նաև նոր կոնստրուկտոր, որը կառուցում է փոփոխականին համապատասխան հանգույց.
Փոփոխության է ենթարկվում նաև արտահայտության արժեքը հաշվարկող 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 Statementinterface{Execute(env *environ.Environment)}
Վերագրման հրամանն իրականացված է Assignment կառուցվածքով, որի name դաշտը փոփոխականի անունն է, իսկ expr դաշտը այն արտահայտությունն է, որի արժեքը միջավայրում պետք է կապել փոփոխականի հետ։
type Assignmentstruct{
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 դաշտը այն արտահայտությունն է, որի արժեքը պետք է հաշվել ու արտածել։
Քերականության մեջ ավելացել են երեք թոքեններ՝ “Ident”, “Print” և “=”։ Իդենտիֆիկատորը դա տառով սկսվող տառաթվային հաջորդականություն է։ ‘print’-ը արտածման հրամանի ծառայողական բառն է։ ‘=’ նիշը վերագրման հրամանի սիմվոլն է։
Ծառայողական բառերի համար սահմանենք մի աղյուսակ, որում իդենտիֆիկատորին համապատասխանեցված է թոքեն։ Այս աղյուսակն օգտագործելու ենք, որպեսզի պարզենք արդոք կարդացած իդենտիֆիկատորը ծառայողական բառ է, թե՝ ոչ։
var keywords = map[string]byte{"print":Print}
Սահմանենք նաև մի օգնական ֆունկցիա, որը դրական պատասխան է տալիս այն դեպքում, երբ արգումենտում տրված սիմվոլը տառ է կամ թվանշան։
Լեքսիկական անալիզատորի Next մեթոդում ավելացնենք մի բլոկ, որը կարդում է տառով սկսվող տառաթվային հաջորդականություն, որոնում է այն ծառայողական բառերի աղյուսակում և, եթե գտնվել է, ապա վերադարձնում է համապատասխան թոքենը, հակառակ դեպքում վերադարձնում է Ident թոքենը։
...if unicode.IsLetter(ch){
iden := s.scanWith(isLetterOrDigit)
tok, iskey := keywords[iden]if!iskey { tok =Ident}return iden, tok
}...
Այս պահին մեր հրամանները կարող են սկսվել կա՛մ իդենտիֆիկատորով, կա՛մ print ծառայողական բառով։ Ես ճյուղավորման համար ընտրել եմ switch կառուցվածքը, որպեսզի հետագայում հեշտ լինի նոր հրամանների վերլուծության ճյուղերն ավելացնելը։
Վերագրման հրամանը վերլուծելու համար հերթականությամբ պետք է ճանաչել իդենտիֆիկատորը, հավասարության նշանը և հաջորդող արտահայտությունը։ Ապա կառուցել և վերադարձնել նոր Assignment հանգույց։
Արտածման հրամանը վերլուծելիս պետք է ճանաչել print ծառայողական բառը, ապա վերլուծել նրան հաջորդող արտահայտությունը։
func (p *Parser) printexpr() astexec.Statement{
p.match(scanner.Print)
ex := p.expr()return astexec.NewPrint(ex)}
Քանի որ նոր քերականության մեջ առաջինը Statement կանոնն է, փոփոխենք Parse մեթոդն այնպես, որ նախ՝ այն վերադարձնի astexec.Statement, ապա՝ վերլուծությունը սկսի ոչ թե expr մեթոդից, այլ statement մեթոդից։
Ձևափոխված երկխոսության ցիկլում ստեղծում ենք նոր Environment օբյեկտ, որում պահվելու են փոփոխականների արժեքները։ Երբ Parse մեթոդը վերադարձնում է հրամանին համապատասխան աբստրակտ քերականական ծառը, կանչում ենք նրա Execute մեթոդը՝ արգումենտում տալով env օբյեկտը որպես կատարման միջավայր։
Հաշվարկիչի այս հերթական զարգացումը նպատակ ունի առաջին քայլն անել պարզագույն REPL ցիկլից դեպի ծրագրավորման լեզվի ինտերպրետատոր։ Նախ՝ լեքսիկական անալիզատորը կփոփոխվի այնպես, որ հրամանային տողի փոխարեն ծրագրի տեքստը ընթերցվի ֆայլից։ Ապա՝ լեզվի հրամանների համակարգը կընդլայնվի ներածման և հաջորդման հրամաններով։
Ձևափոխենք լեքսիկական անալիզատորն այնպես, որ ծրագրի նիշերն ընթերցվեն ոչ թե տողից, այլ bufio.Reader օբյեկտից։ Scanner կառուցվածքի նոր տեսքն ահա այսպիսինն է.
type Scannerstruct{
source *bufio.Reader
line int}
Որտեղ line փոփոխականը նախատեսված է տողի համարը պահելու համար։ Այն մեզ հարկավոր է լինելու քերականական սխալների տեղը նշելիս։
Փոփոխության է ենթարկվում նաև Scanner-ի կոնստրուկտորը.
Հոսքից նիշեր կարդալիս անհրաժեշտ է մի ֆունկցիա, որը վերադարձնում է հերթական նիշը, բայց այն չի հեռացնում հոսքից։ Եթե հոսքն արդեն դատարկ է, ապա վերադարձնում է 0։
func (s *Scanner) peek() rune {
br, ok := s.source.Peek(1)if ok !=nil{return0}return rune(br[0])}
Մեկ այլ ֆունկցիա կարդում և վերադարձնում է հերթական սիմվոլը։ Այս դեպքում նույնպես, եթե հոսքն արդեն դատարկ է, ապա վերադարձնում է 0։
func (s *Scanner)char() rune {
ch, _, ok := s.source.ReadRune()if ok !=nil{return0}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 օբյեկտի ցուցիչ։
Հաջորդման հրաման (կամ հրամանների հաջորդման կառուցվածք) ասելով կհասկանանք ծրագրի տեքստում իրար հետևից գրված հրամանները, որոնք բաժանված են “;” նիշով։ Պետք է ուշադրություն դարձնել, որ ոչ թե ամեն մի հրաման ավարտվում է “;” նիշով, այլ նրանով հրամաններն իրարից բաժանվում են։
Sequence կառուցվածքը պարունակում է միակ children դաշտը, որը պարունակում է հաջորդականություն կազմող հրամանների ցուցիչները։
type Sequencestruct{
children *list.List}
Կոնստրուկտորը պարզապես արժեքավորում է children դաշտը։
Ներածման հրամանը որոշվում է input ծառայողական բառով և նրան հետևող իդենտիֆիկատորով։ Այն օգտագործողից պահանջում է ստեղնաշարից ներմուծել ամբողջ թվի արժեքը։
Աբստրակտ քերականական ծառում ներածման հրամանի հանգույցն ունի հետևյալ տեսքը, որտեղ name դաշտը այն փոփոխականի անունն է, որի համար պետք է կարդալ արժեքը։
Ներածման հրամանը կատարելու համար ստեղծում ենք bufio.Reader օբյեկտ՝ os.Stdin ֆայլի հետ կապված։ Ապա կարդում ենք տող, նրանով արժեքավորում նոր big.Int օբյեկտ և փոփոխականի անունի հետ միասին ավելացում միջավայրում։
Parser կառուցվածքի inputval մեթոդը նախ չանաչում է scanner.Input թոքենը, ապա իդենտիֆիկատորը, վերջինիս համապատասխան լեքսեմն էլ օգտագործելով ստեղծում է նոր astexec.Input օբյեկտ։
main փաթեթի main ֆունկցիայում նախ ստուգում ենք, որ հրամանայի տողում տրված արումենտները լինեն ճիշտ երկու հատ՝ len(os.Args) != 2։ Ապա փորձում ենք os.Open ֆունկցիայով բացել ծրագրի ֆայլը՝ անհաջողության դեպքում տալով հաղորդագրություն։
Ինտերպրետատորի զարգացման այս քայլում ես պլանավորել էի իրականացնել ճյուղավորման և կրկնման հրամանները։ Բայց, քանի որ համեմատման գործողությունները սերտորեն կապված են դրանց հետ, ես նախ ընդլայնեցի արտահայտություններն այնպես, որ նրանք ընդգրկեն համեմատման վեց գործողություններ, ապա նոր միայն ընդլայնեցի հրամանների համակարգը ճյուղավորման և կրկնման հրամաններով։
Քանի որ սրանով թվաբանական ու համեմատման գործողությունների քանակն իրար հետ վերցրած հասավ տասի, ես որոշեցի լեքսեմներին թոքեններ համապատասխանեցնելու համար սահմանել մի հեշ աղյուսակ․
var operations = map[string]byte{"+":Add,"-":Sub,"*":Mul,"/":Div,"=":Equal,"<>":NotEq,">":Great,">=":GrEq,"<":Less,"<=":LsEq,}
Իսկ գործողություններ կազմող սիմվոլները ճանաչելու համար սահմանեցի isOpChar ֆունկցիան, որպեսզի հնարավորություն ունենամ գործողությունների նշաններ կարդալ scanWith մեթոդով․
Քանի որ համեմատումները բինար գործողություններ են, նրանց համապատասխան ծառի հանգույցներ կստեղծենք 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
}
Երկու նոր հրամանները, որոնք անհրաժեշտ են քիչ թե շատ կարգին ալգորիթմներ գրելու համար դրանք ճյուղավորման ու կրկնման հրամաններն են։ Քերականական տեսակետից նրանք ունեն այսպիսի տեսք.
astexec փաթեթում ավելանում են Branching և Looping կառուցվածքներն իրենց կոնստրուկտորներով ու Execute մեթոդներով։
Branching կառուցվածքը, որ նախատեսված է ճյուղավորման հրամանի համար, պարունակում է cond դաշտը որպես պայմանի արտահայտություն, thenpart դաշտը որպես հրամանների հաջորդականություն, որոնք պիտի կատարվեն այն դեպքում, երբ cond արտահայտության արժեքը հաշվարկվել է զրոյից տարբեր, և elsepart դաշտը որպես հարմանների հաջորդականություն, որոնք կատարվում են cond արտահայտության զրո հաշվարկված արժեքի դեպքում։
type Branchingstruct{
cond *asteval.Expression
thenpart, elsepart Statement}
Կոնստրուկտորը ստանում է միայն երկու արգումենտ՝ cond և thenpart դաշտերի համար, իսկ elsepart դաշտն արժեքավորվում է առանձին SetElse մեթոդով։
Ճյուղավորման հրամանի կատարումը շատ պարզ է։ Նախ հաշվարկվում է cond արտահայտության արժեքը, ապա, եթե այն տարբեր է զրոյից՝ կատարվում է thenpart հրամանների հաջորդականությունը, հակառակ դեպքում՝ elsepart հաջորդականությունը։
func (b *Branching)Execute(env *environ.Environment){
ex := b.cond.Evaluate(env)if0!= ex.Cmp(big.NewInt(0)){
b.thenpart.Execute(env)}else{
b.elsepart.Execute(env)}}
Կրկնման հրամանը մոդելավորող Looping կառուցվածքում cond դաշտը ցիկլի կատարման պայմանն է, իսկ body դաշտը՝ ցիկլի մարմինը։ Կոնստրուկտորը պարզապես արժեքավորում է այդ դաշտերը։
type Loopingstruct{
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)for0!= 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 մեթոդում ավելացնել նրանց համապատասխան ճյուղերը։
Լեքսիկական կամ նիշային վերլուծությունը տեքստի մշակման այն փուլն է, երբ, ըստ նախապես տրված կանոնների, տեստը տրոհվում է առանձին հատվածների և ամեն մի հատվածի վերագրվում է իր տիպին համապատասխան պիտակ։ Օրինակ, թող որ տրված են ինչ-որ ծրագրավորման լեզվի ծրագրերի լեքսիկական վերլուծության հետևյալ մի քանի (ոչ լրիվ) կանոնները․
if, then, else, print բառերը ծառայողական բառեր են՝ ամեն մեկն իր անունով,
Տառով սկսվող և տառերից ու թվերից բաղկացած սիմվոլների անընդհատ հաջորդականությունը իդենտիֆիկատոր է,
Թվանշանների անընդհատ հաջորդականությունը ամբողջ թիվ է,
Երկու չակերտներով պարփակված և չակերտ չպարունակող տեքստի հատվածը տող է,
>, >=, <, <= և այլն, համեմատման գործողություններ են՝ ամեն մեկն իր անունով։
Եվ պետք է ըստ այս կանոնների լեքսիկական վերլուծության ենթարկել հետևյալ տեքստը․
if num >=10thenprint"Test done"
Սիմվոլների առաջին անընդհատ հատվածը if բառն է, որը, ըստ տրված կանոնների, ծառայողական բառ է։ Հաջորդ հատվածը num բառն է, որը չկա ծառայողական բառերի ցուցակում և, ըստ երկրորդ կանոնի, պետք է համարել իդենտիֆիկատոր։ >= սիմվոլների հաջորդականությունը մեծ է կամ հավասար համեմատման գործողությունն է, ըստ հինգերորդ կանոնի։ Ըստ երրորդ կանոնի 10 հաջորդականութթյունը ամբողջ թիվ է։ then և print բառերը նորից ծառայողական բառեր են։ Իսկ Test done հատվածը, ընդ որում՝ առանց ընդգրկող չակերտների, տող է։
Ընդունված տերմինաբանությամբ նախապես տրված տեքստից առանձնացված հատվածները կոչվում են լեքսեմներ (lexeme), իսկ նրանց համապատասխանեցված պիտակները՝ տոկեններ (token) կամ թոքեններ։ Եթե բերված օրինակի համար կազմենք լեքսեմների ու թոքեների աղյուսակը, ապա այն կունենա այսպիսի տեսք․
Լեքսեմ
Թոքեն
if
IF
num
IDENTIFIER
>=
GE
10
INTEGER
then
THEN
print
PRINT
Test done
STRING
Լեքսիկական վերլուծության ժամանակ նիշ առ նիշ անցում է կատարվում վերլուծության ենթական տեքստով և փորձ է կատարվում ճանաչել վերլուծության կանոններին համապատասխանող նիշերի ամենաերկար հաջորդականությունը։ Կարելի է լեքսիկական անալիզատորը ներկայացնել որպես վերջավոր ավտոմատ, և հմնականում հենց այդպես էլ արվում է։ Ավտոմատի սկզբնակական վիճակից, կողմնորոշվելով հերթական սիմվոլով, ընտրվում է վերլուծության համապատասխան կանոնը։ Այնուհետև ավտոմատն աշխատում և փոխում է իր վիճակներն այնքան ժամանակ, քանի դեռ չի հասել վերջնական վիճակի կամ քանի դեռ չի ավարտվել վերլուծության ենթարկվող տողը։ Առաջին դեպքում համարվում է, որ թոքենը ճանաչվել է, և ավտոմատը վերադառնում է սկզբնական վիճակին՝ նոր թոքեն ճանաչելու փորձ կատարելու համար։ Երկրորդ դեպքում, երբ ավարտվել է տողի ընթերցումը, բայց ավտոմատը չի հասել որևէ վերջնական վիճակի, կա՛մ կարելի է տալ սխալի մասին ազդանշան, կա՛մ պահանջել ևս մի տող՝ վերլուծությունը շարունակելու համար։
Ստորև բերված է մի դետերմինացված վերջավոր ավտոմատի (DFA) ուրվապատկեր, որը «կարողանում է ճանաչել» իդենտիֆիկատորները, չակերտների մեջ վերցրած տողերը և ամբողջ թվերը։
Հերթական սիմվոլը կարդալուց հետո «1» համարով վիճակում որոշում է կայացվում, թե որ ուղղությամբ պետք է շարժվել, իսկ «3», «4» և «5» վիճակները ցույց են տալիս, թե ինչ տիպի տեքստ է ճանաչվել։
Լեքսիկական վերլուծության ժամանակ հոսքից տվյալների ընթերցումը կատարվում է նիշ առ նիշ։ Այդ նիշերն իրար կցելու և ամբողջական տող ստանալու համար է սահմանված char-list-to-string ֆունկցիան․
Ամենատարածված դեպքերն են, երբ երբ հոսքից պետք է կարդալ կա՛մ որևէ տիպի նիշերի շարք, կա՛մ տրված երկու նիշերի միջև պարփակված տեքստ։ Օրինակ, եթե հոսքի ընթացիկ նիշը տառ է՝ alpha-char-p, ապա պետք կարդալ նրան հաջորդող բոլոր տառերն ու թվանշանները՝ alphanumericp, ապա պարզել կարդացածը իդենտիֆիկատո՞ր է, թե՞ ծառայողական բառ։ Տրված պրեդիկատին բավարարող նիշերի շղթա կարդալու համար սահմանված է scan-sequence ֆունկցիան։
(defun scan-sequence (fu sm)(char-list-to-string(loop for ch =(read-char sm nil)until(null ch)while(funcall fu ch)
collect ch
finally(when ch (unread-char ch sm)))))
Իսկ տրված երկու նիշերի մեջ պարփակված նիշերի շարքը կարդալու համար սահմանված է scan-quoted ֆունկցիան։
(defun scan-quoted (co ci sm)(if(char= co (read-char sm nil))(prog1
(scan-sequence #'(lambda (c) (char/= c ci)) sm)(read-char sm nil))))
Հիմանականում անհրաժեշտ է լինում ընթերցման հոսքից հեռացնել իմաստ չպարունակող բացատանիշերը (white spaces)։ Որպեսզի հնարավոր լինի scan-sequence ֆունկցիայով կարդալ իրարա հաջորդող բացատանիշերը, սահմանված են space-char-p պրեդիկատը և skip-white-spaces ֆունկցիան։
(defun space-char-p (c)(or(char= c #\Newline)(char= c #\Tab)(char= c #\Space)(char= c #\Return)))(defun skip-white-spaces (stream)(scan-sequence #'space-char-p stream))
Իդենտիֆիկատորներն ու ծառայողական բառերը կարդացվոլու են նույն կանոնով։ +keywords+ ցուցակում առանձնացված են այն լեքսեմները, որոք ծառայողական բառեր են, և նրանցից ամեն մեկին համապատասխանեցված է իր թոքենը։
oper-char-p պրեդիկատը դրական պատասխան է տալիս իր արգումենտում տրված այն նիշերի համար, որոնցով կարելի է կազմել հարաբերության կամ թվաբանական գործողություններ։
Իսկ +operations+ ցուցակում հավաքված են այն գործողությունների նշանները, որոնք կազմված են open-char-p պրեդիկատին բավարարող նիշերից (կամ դրանց համակցությունից)։
Պարզելու համար, թե արդյոք կարդացած լեքսեմը պատկանո՞ւմ է +keywords+ կամ +operations+ ցուցակներից որևէ մեկին, սահմանված է known-lexeme ֆունկցիան։
(defun known-lexeme (lexeme pairs &optional (default:lex-unknown))(let((res (find lexeme pairs :test #'equal :key #'first)))(if res res (cons lexeme default))))
Եվ վերջապես, լեքսիկական (նիշային) վերլուծություն կատարող scan-lexeme հիմնական ֆունկցիան։ Այն նախապես ընթերցման հոսքից հեռացնում է բացատանիշերը։ Ապա, ըստ դեն նետված բացատանիշերին հաջորդող առաջին նիշի, որոշում է կայացնում, թե ինչ լեքսեմ պետք է կարդալ։
Tcl լեզվում պրոցեդուրները (կամ ֆունկցիաները) սահմանվում են proc հրամանի միջոցով։ Այն ստանում է երեք արգումենտ. ա) սահմանվող պրոցեդուրայի անունը, բ) արգումենտների ցուցակը, և գ) մարմինը։ Օրինակ, տրված x և y թվերից մեծը որոշող ֆունկցիան կարելի է սահմանել հետևյալ կերպ.
proc max { x y }{if{ $x > $y }then{return $x
}return $y
}
Կամ, եթե չենք ուզում օգտագործել if հրամանը.
proc max { x y }{return[expr {$x > $y ? $x : $y}]}
Այս երկու օրինակներում էլ return հրամանն օգտագործված է ֆունկցիայի մարմնից արժեք վերադարձնելու համար։ Tcl պրոցեդուրաները կառուցված են այնպես, որ եթե նրա մարմնում, կամ կատարման որևէ ճյուղում, որը բերում է պրոցեդուրայի ավարտի, բացակայում է return հրամանը, ապա ֆունկցիայի վերադարձրած արժեք է համարվում դրա կատարման ժամանակ վերջին արտահայտության հաշվարկված արժեքը։ Սա հաշվի առնելով, կարող են նախորդ օրինակը գրել առանց return հրամանի.
proc max { x y }{
expr {$x > $y ? $x : $y}}
Եթե պրոցեդուրան սպասում է միայն մեկ արգումենտ, ապա սահմանման ժամանակ այդ միակ ֆորմալ արգումենտի անունը կարելի է գրել առանց ձևավոր փակագծերի։ Օրինակ, Սահմանենք մի պրոցեդուրա, որը ստուգում է տրված բառի պալինդրոմ լինելը.
Իսկ եթե արգումենտների ցուցակում վերջին կամ միակ արգումենտի անունը args է, ապա նրա միջոցով, որպես ցուցակ, պրոցեդուրայի մարմնին են փոխանցվում կամայական թվով արժեքներ։ Օրինակ, ահմանենք մի ֆունկցիա, որը հաշվում է երկու և ավելի թվերի թվաբանական միջինը.
proc average { a b args }{set sum [expr {$a + $b}]foreach n $args {set sum [expr {$sum + $n}]}return[expr {$sum /(2+[llength $args])}]}
Ահա նաև այս պրոցեդուրայի օգտագործման մի քանի օրիակ.
average 1.12.2;# => 1.6500000000000001
average 1.12.25.54.43.3;# => 3.3
average 1.22.33.4;# => 2.3000000000000003
proc հրամանը հնարավորություն է տալիս սահմանել նաև լռելությոն (default) արժեքով արգումենտներ ունեցող պրոցեդուրաներ։ Այդ դեպքում արգումենտը տրվում է երկու տարրերի ցուցակի տեսքով, որոնցից առաջինը ֆորմալ արգումենտի անունն է, իսկերկրորդը՝ նրա լռելության արժեքը։ Օրինակ, սահմանենք մի ֆունկցիա, որը վերադարձնում է y թվի n աստիճանի արմատը՝ n√y, եթե n-ը տրված չէ, այն համարվում է 2։
Scheme ծրագրավորման լեզվում պրոցեդուրաները սահմանվում են lambda ծառայողական բառով, որին հետևում են արգումենտների ցուցակը և պրոցեդուրայի մարմինը կազմող արտահայտությունները։ Օրինակ, կարող եմ սահմանել տրված երկու թվերի քառակուսիների գումարը հաշվող պրոցեդուրա․
(lambda(x y)(+(* x x)(* y y)))
Սա x և y ֆորմալ արգումենտներով պրոցեդուրա է։ Եթե ուզենամ այն կիրառել 3 կամ 4 արգումենտների նկատմամբ, ապա պետք է գրեմ․
((lambda(x y)(+(* x x)(* y y)))34);=>25
Այս անհարմար գրառումից խուսափելու համար կարող եմ պրոցեդուրային անուն տալ՝ define ծառայողական բառի օգնությամբ այն կապելով որևէ սիմվոլի հետ.
(define sum-squares
(lambda(x y)(+(* x x)(* y y))))
Եվ արդեն կարող եմ պրոցեդուրան արգումենտների նկատմամբ կիրառել sum-squares անունով։
(sum-squares 34);=>25
Պրոցեդուրայի սահմանումն ավելի համառոտ կարելի է գրել առանց lambda ծառայողական բառի օգտագործման՝ define սահմանման առաջին արգումենտը դարձնելով ցուցակ, որի առաջին տարրը սահմանվող պրոցեդուրայի անունն է, իսկ հաջորդողները՝ ֆորմալ արգումենտները։ Օրինակ,
(define (sum-squares x y)(+(* x x)(* y y)))
Ես նախնտրում եմ lambda-ի օգտատգործմամբ տարբերակը։ Կարծում եմ, որ դա ավելի համահունչ է define բառի հետ։
Հիմա, ենթադրենք, պետք է սահմանել պրոցեդուրա, որը իրար է գումարում տրված թվերը։ Առաջին միտքն այն է, որ սհամանել մեկ արգումենտով պրեցեդուրա և նրան փոխանցել թվերի ցուցակը։ Օրինակ այսպես.
Սա ռեկուրսիվ պրոցեդուրա է, որ 0 է վերադարձնում դատարկ ցուցակի դեպքում, իսկ ոչդատարկ ցուցակների համար ցուցակի առաջին տարրը գումարում է պոչի տարրերի գումարին։ Այս պրոցեդուրան պետք է կիրառել հետևյալ կերպ.
Բայց ավելի բնական կլիներ, եթե ես կարողանայի գրել այսպիսի արտահայտություններ.
(sum);=>0(sum 1);=>1(sum 1234);=>10
Այլ կերպ ասած՝ ունենալ պրոցեդուրա, որի արգումենտների քանակը ֆիքսված չէ. դրանք կարող են կա՛մ բացակայել, կա՛մ լինել անորոշ քանակի։
Երբ lambda կառուցվածքի առաջին արգումենտը տրված է որպես ատոմ (այլ ոչ թե որպես ցուցակ), ապա պրոցեդուրայի արգումենտը համարվում է անորոշ քանակի։ Օրինակ, հիշատակված sum պրոցեդուրան, օգտագործելով sum-list պրոցեդուրան, կարելի է սահմանել հետևյալ կերպ.
(define sum
(lambda nums
(sum-list nums)))
Երբ այս կերպ սահմանված պրոցեդուրան կիրառվում է արգումենտների նկատմամբ, այդ արգումենտներից կազմվում է ցուցակ և փոխանցվում է պրոցեդուրային։ Բնականաբար, պրոցեդուրայի մարմնում պետք է ֆորմալ արգումետի հետ աշխատել որպես ֆունկցիա։ Օրինակ, եթե sum պրոցեդուրան սահմանելու լինեի առանց sum-list պրոցեդուրայի օգտագործման, ապա պետք է գրեի.
(define sum
(lambda nums
(if(null? nums)0(+(car nums)(apply sum (cdr nums))))))
apply հրամանն օգտագործված է այն պատճառով, որ sum պրոցեդուրայի ռեկուրսիվ կանչի ժամանակ (cdr nums) ցուցակը նրան փոխանցվելու է որպես նոր ցուցակի միակ տարր։
Երկու սերտ հասկացություններ՝ բարձր կարգի ֆունկցիա և անանուն ֆունկցիա, որոնք լայնորեն կիրառվում են ֆունկցիոնալ ծրագրավորման մեջ, որոնց տրամադրած մեխանիզմները հնարավորություն են տալիս կազմակերպել ավելի բնական, ավելի գեղեցիկ, ավելի ընթեռնելի ծրագրեր։
Ֆունկցիան կոչվում է բարձր կարգի, եթե նրա արգումենտներից գոնե մեկը ֆունկցիա է, կամ նրա վերադարձրած արժեքն է ֆունկցիա։
Ես ուզում եմ բարձր կարգի ֆունկցիաները ցուցադրել տրված ֆունկցիայի որոշյալ ինտեգրալի թվային հաշվման օրինակով։ Ենթադրենք պետք է իրականացնել integral ֆունկցիան, որն արգումենտում ստանում է f(x) ինտեգրվող ֆունկցիան և [a;b] ինտեգրման միջակայքը։
integral(f,a,b)=b∫af(x)dx
Այս integral ֆունկցիան երկրորդ կարգի է, որովհետև նրա f արգումենտը առաջին կարգի ֆունկցիա է։
Ինտեգրալի թվային հաշվման համար ընտրենք սեղանների մեթոդը, որի դեպքում ինտեգրման միջակայում f ֆունկցիայի գրաֆիկը մոտարկվում է ուղիղ գծով, իսկ ինտեգրալի արժեքը ընդունվում է (a,0), (a,f(a)), (b,f(b)) և (0,b) գագաթներով սեղանի մակերեսին հավասար։
trapezoid(f,a,b)=(b−a)f(a)+f(b)2
Common Lisp լեզվով այս բանաձևը ծրագրավորվում է հետևյալ կերպ.
(defun trapezoid (f a b)(*(- b a)(/(+(funcall f a)(funcall f b))2.0)))
integral ֆունկցիայի արգումենտում տրված f ֆունկցիա-արգումենտը ֆունկցիայի մարմնում օգտագործված է funcall ֆունկցիայի միջոցով, որը տրված ֆունկկցիա-օբյեկտը կիրառում է տրված արգումենտների նկատմամբ։
C++11 լեզվով նույն integral ֆունկցիան կարելի է գրել հետևյալ կերպ.
double trapezoid( std::function<double(double)> f,double a,double b ){return(b - a)*((f(a)+ f(b))/2);}
Եթե այս ֆունկցիայով հաշվենք, օրինակ f(x)=x2 ֆունկցիայի ինտեգրալը [−1;1] միջակայքում, ապա կստանանք 2 արժեքը՝ իրական 2/3-ի փոխարեն։
Պարզ է, որ սեղանների մեթոդը կարող է բավարար ճշտություն ապահովել միայն այն դեպքում, երբ ինտեգրման միջակայքը շատ փոքր է։ Օգտագործելով այդ փաստը, սահմանեմ integral ֆունկցիան, որի ε արգումենտը ցույց է տալիս ինտեգրման միջակայքի ամենամեծ թույլատրելի չափը։ Այն դեպքում, երբ b−a>ε, միջակայքը կկիսեմ երկու հավասար մասերի և իրար կգումարեմ այդ երկու մասերում հաշված ինտեգրալների արժեքները։ Այլ կերպ ասած, ինտեգրալի հաշվման նոր բանաձևն է հետևյալը.
integral(f,a,b,ε)={trapezoid(f,a,b),|b−a|≤ε,integral(f,a,a+b2,ε)+integral(f,a+b2,b,ε),|b−a|>ε.
Այս integral ֆունկցիան նույնպես երկրորդ կարգի է, քանի որ նրա արգումենտներում ամենաբարձր կարգը առաջինն է։
integral ֆունկցիայի սահմանումը Common Lisp լեզվով ունի այսպիսի տեսք.
(defun integral (f a b &optional (eps 0.001))(if(<=(- b a) eps)(trapezoid f a b)(let((m (/(+ a b)2)))(+(integral f a m eps)(integral f m b eps)))))
Նույն ֆունկցիան C++11 լեզվով կունենա հետևյալ տեսքը։
using mathfunc = std::function<double(double)>;double integral( mathfunc f,double a,double b,double eps =0.001){if( b - a <= eps)return(b - a)*((f(a)+ f(b))/2);auto m((a + b)/2);return integral(f, a, m, eps)+ integral(f, m, b, eps);}
Հիմա, ենթադրենք, ուզում եմ հաշվել f(x)=x3 ֆունկցիայի ինտեգրալը [0;2] միջակայքում: Նախ սահմանեմ այդ ֆունկցիան Lisp լեզվով.
(defun f (x)(* x x x))
Ապա integral ֆունկցիայի օգնությամբ հաշվեմ սրա ինտեգրալը տրված միջակայքում.
(integral #'qub 0 2.0) ; => 4.0
Եթե պետք լինի հաշվել, օրինակ, f(x)=x2−3x ֆունկցիայի ինտեգրալը, ապա սա նույնպես պետք է սահմանել
(defun f (x)(-(* x x)(* x 3)))
ապա integral ֆունկցիան կիրառել այս f ֆունկցիայի նկատմամբ։ Բայց անանուն ֆունկցիաների մեխանիզմը հնարավորություն է տալիս խուսափել ինտեգրվող ֆունկցիայի առանձին սահմանումից։ Օրինակ, այս վեջին ֆունկցիան [−2;1] միջակայքում ինտեգրելու համար կարելի է գրել.
(integral #'(lambda (x) (- (* x x) (* x 3))) -2 1)
Այստեղ integral ֆունկցիայի կանչի մեջ lambda մակրոսով ստեղծվել է ինտեգրվող ֆունկցիային համապատասխան անանուն ֆունկցիա։
C++11 լեզվում նույնպես կարելի է գրել համարժեք արտահայտություն.
որտեղ անանուն ֆունկցիան սահմանված C++11 լեզվի []()->{} կառուցվածքով։
Բայց սեղանների մեթոդը ինտեգրալի հաշվման միակ մեթոդը չէ։ Օրինակ, f ֆունկցիան [a;b] հատվածի վրա կարելի է մոտարկել ոչ թե ուղիղ գծով, այլ պարաբոլով։ Այս մեթոդը կոչվում է Սիմպսոնի մեթոդ և ներկայանում է հետևյալ բանաձևով.
simpson(f,a,b)=b−a6(f(a)+4f(a+b2)+f(b))
Եվ ինտեգրալի հաշվման թվային մեթոդը նույնպես կարելի է տալ integral ֆունկցիային որպես արգումենտ։ Այդ դեպքում կստանանք մի նոր, երրորդ կարգի ֆունկցիա.
integral(method,f,a,b,ε)={method(f,a,b),|b−a|≤ε,integral(f,a,a+b2,ε)+integral(f,a+b2,b,ε),|b−a|>ε.
Ծրագրավորենք այս ֆունկցիան Common Lisp լեզվով.
(defun integral (method f a b &optional (eps 0.001))(if(<(- b a) eps)(funcall method f a b)(let((m (/(+ a b)2)))(+(integral method f a m eps)(integral method f m b eps)))))
Եվ C++11 լեզվով։
using method = std::function<double(mathfunc,double,double)>>;double integral( method r, mathfunc f,double a,double b,double delta =0.001){if( b - a < delta )return r(f, a, b);auto m((a + b)/2);return integral(r, f, a, m, delta)+ integral(r, f, m, b, delta);}
Այս գրառման մեջ Tcl լեզվով իրականացված է 1-ից 999999 միջակայքի բնական թվերի բառային արտահայտման ալգորիթմը։ (Ամբողջական ֆայլը։)
## միավորների անունները հայերեն#set ONES {""մեկերկուերեքչորսհինգվեցյոթութինը}## տասնավորների անունները հայերեն#set TENS {""տասքսաներեսունքառասունհիսունվաթսունյոթանասունութսունիննսուն}## n-ը պատկանում է [a; b] միջակայքին#
proc is_in { n a b }{return[expr {($n >= $a)&&($n <= $b)}]}## վերադարձնում է երկու տարրերի ցուցակ, որոնցից առաջինը# (ds / dr) քանորդն է, իսկ երկրորդը՝ (ds % dr) մնացորդը#
proc divr { ds dr }{return[list [expr {$ds / $dr}][expr {$ds % $dr}]]}## Հիմնական ձևափոխություն#
proc number_to_words num {# եթե թիվը 0-9 միջակայքում է, ...if[is_in $num 09]{# ... ապա վերադարձնել միավորների ցուցակի համապատասխան տարրըreturn[lindex $::ONES $num]}# եթե թիվը 10-99 միջակայքում է, ...if[is_in $num 1099]{# ... ապա առանձնացնել տասնավորի ու միավորի նիշը, ...
lassign [divr $num 10] a b
# ... ՛տաս՛-ով սկսվող բառերի համար տասնավորի ու միավորի # միջև պետք է գրել ՛ն՛ տառը, օրինակ, ՛տասներկու՛, ...set s [expr {$a ==1&& $b !=0?{ն}:{}}]# ... վերադարձնել տասնավորների ու միավորների համապատասխան # տարրերից կազմված արտահայտությունըreturn"[lindex $::TENS $a]${s}[lindex $::ONES $b]"}# եթե թիվը 100-999 միջակայքում է, ...if[is_in $num 100999]{# ... ապա առանձնացնել հարյուրավորի նիշը և 100-ի բաժանելու մնացորդը, ...
lassign [divr $num 100] a b
# ... միավորների ցուցակից վերցնել հարյուրավորի նիշին համապատախան բառը,# նրան կցել ՛հարյուր՛ բառը, իսկ 100-ի բաժանելու մնացորդի վրա նորից կիրառել# number_to_words ֆունկցիանreturn"[lindex $::ONES $a] հարյուր [number_to_words $b]"}# եթե թիվը 1000-999999 միջակայքում է, ...if[is_in $num 1000999999]{# ... ապա այն տրոհել երկու կտորի՝ 1000-ին բաժանելու քանորդ և մնացորդ, ...
lassign [divr $num 1000] a b
# ... ստացված կտորները ձևափոխել number_to_words ֆունկցիայով, # և իրար կցել՝ արանքում դնելով ՛հազար՛ բառըreturn"[number_to_words $a] հազար [number_to_words $b]"}return""}
Տեստավորման փորձ։
## գեներացնել [0;N) միջակայքի պատահական ամբողջ թիվ#
proc random N {return[expr {int(rand()* $N)}]}## number_to_words ֆունկցիան փորձարկել 10 պատահական թվերով#for{set i 0}{$i <10}{incr i}{set k [random 1000000]set v [number_to_words $k]
puts "$k -- $v"}
Ենթադրենք տրված է pixmap պատկեր պարունակող պարզեցված ֆայլ և պահանջվում է գրել մի ծրագիր, որը պատկերը կխոշորացնի տրված գործակցով։ Օրինակ, ստորև բերված են հայերեն "Բ" տառի սկզբնական pixmap պատկերը և նրա երկու անգամ խոշորտացված տարբերակը։
Ծրագիրը պետք կարդա ձախ կողմի պատկերը պարունակող ֆայլը և ստեղծի նոր ֆայլ՝ աջ պատկերի պարունակմամբ։
Թող ամբողջ աշխատանքը կատարող պրոցեդուրան կոչվի enlarge, որը ստանում է երկու արգումենտ. պատկերը պարունակող ֆայլի անունը՝ pixmap և խոշորացման գործակիցը՝ sc։
proc enlarge { pixmap {sc 2}}{
Հետո պետք է բացել ֆայլը, նրա պարունակությունը կարդալ որևէ փոփոխականի մեջ, ապա նորից փակել ֆայլը։ Tcl լեզվում ֆայը բացվում է open պրոցեդուրայով, որի առաջին արգումենտը ֆալի անունն է, իսկ երկրորդով որոշվում է, թե ինչ գործողություն է կատարվելու ֆայլի հետ՝ գրել (w), կարդալ (r) և այլն։ open պրոցեդուրան վերադարձնում է ֆայլային հոսք, որից կարելի է կարդալ, կամ նրա մեջ կարելի է գրել։
set inp [open $pixmap r]
Ֆայլի պարունակությունը ամբողջությամբ կարդում է read պրոցեդուրան։ Այն ստանում է կարդալու համար բացված ֆայլային հոսք և վերադարձնում է հոսքի ամբողջ պարունակությունը։
set img [read $inp]
Դե, իսկ close պրոցեդուրան փակում է բացված ֆայլային հոսքը։
close $inp
Պատկերը n անգամ խոշորացնելու համար պետք է նրա ամեն մի կետը հորիզոնական և ուղղահայաց ուղղություններով ընդարձակել n անգամ։ Դրա համար պետք է վերցնել սկզբնական պատկերի ամեն մի տողը և, n անգամ կրկնելով նրա ամեն մի սիմվոլը, ստանալ նոր տող։ ՈՒղղահայաց ընդարձակման համար պետք է n անգամ կրկնել կառուցված տողը։
Արդյունքը կուտակելու համար նախատեսենք result փոփոխականը որպես դատարկ ցուցակ։
set result [list]
Քանի որ տրված ֆայլի ամբողջ պարունակությունը կարդացվել էր img փոփոխականի մեջ, պետք է այն split պրոցեդուրայով տրոհել տողերի ու իտերացիա կազմակերպել տողերով։ split պրոցեդուրայի երկրորդ արգումենտով տրվում է այն բաժանիչը, ըստ որի պետք է տողը տրոհել ցուցակի։
foreach line [split $img \n]{
Տողի սիմվոլներով իտերացիայի համար այն կարող ենք սիմվոլների ցուցակի վերածել split պրոցեդուրայով և ցուցակով անցնել foreach պրոցեդուրայով։ Հերթական սիմվոլը դիտարկելիս string repeat հրամանով այն պետք է բազմապատկել պահանջված քանակով ու կցել կառուցվող տողին՝ str փոփոխականին: Իսկ բոլոր սիմվոլներով անցնելուց հետո կառուցված տողին կցենք նոր տողի սիմվոլը։