Pages

Friday, 7 September 2012

computer language ကိုယ္ပိုင္တီထြင္ျခင္း +6

Function Support

ဘယ္ language မွာမဆို function ပါမွ တကယ္ program ေကာင္းေကာင္းေရးလို႔ရမွာပါ။ ဒီေတာ့ ကၽြန္ေတာ္တို႔ language မွာလဲ function မပါလို႔ ဘယ္ျဖစ္ပါ့မလဲ။ မဟုတ္ဘူးလား။ ဒီ chapter မွာကၽြန္ေတာ္တို႕ assembly language မွာ function support ကိုေရးၾကည့္ရေအာင္ပါ။

Assembly language မွာေတာ့ function အစား GOTO ကိုသုံးၿပီးေရးသြားလို႔ေတာ့ ရပါတယ္။ ဒါေပမဲ့ ခုကၽြန္ေတာ္တို႕က high level language အထိသြားမွာဆိုေတာ့၊ function support ကိုတခါတည္း assembly မွာကတည္းက ထည့္ထားလိုက္ပါမယ္။ ဒါမွ high level language ကို compile လုပ္တဲ့အခါ ပိုလြယ္သြားမွာပါ။

ဒီေနရာမွာ function ဆိုတာ တကယ္ေတာ့ GOTO ပါဘဲ။ ေအာက္က C codes ကိုၾကည့္ပါ၊
void A() {    cout  "Hello";
};

A();
အဲဒါကို Assembly နဲ႔ျပန္ေရးၾကည့္ရေအာင္၊
// call the function A at line no 150110 goto     150
// function A()

150 TALK

// return from function

151 goto     111

............
ဒီ assembly codes ဟာ function တခုကို call လုပ္တဲ့ အေျခခံ code လို႔ေျပာလို႔ရပါတယ္။ ဒါေပမဲ့ သူ႕မွာ အားနဲခ်က္တခုရွိပါတယ္။ Line no 151 ကိုၾကည့္ပါ။ Function ကေန return ျပန္ဖို႔အတြက္ သူ႕ကို ေခၚထားတဲ့ line number (ဒီေနရာမွာ 111) ကိုသိဖို႔လိုပါတယ္။ ဒါမွ function ကေန ေခၚထားတဲ့ line ကို GOTO နဲ႔ return ျပန္လို႔ရမွာ မဟုတ္လား။ ဒါေၾကာင့္ ကၽြန္ေတာ္တို႔ function A() ကို line number 110 ကေန မေခၚဘဲ ဥပမာ၊ 200 ကေနေခၚရင္ မရပါဘူး။ ဒီအတြက္ 200 ကို return ျပန္ေအာင္ 151 က GOTO statement ကိုျပန္ျပင္ေရးရပါမယ္။

ဒီအတြက္ solution က သူ႕ကိုေခၚထားတဲ့ line number ကို stack ေပၚမွာအရင္ save လုပ္ထားပါမယ္။ ၿပီးရင္ function ကေန return ျပန္တဲ့အခါ အဲဒီ stack ေပၚက line number ကိုျပန္ဖတ္ၿပီး အဲဒီေနရာကို return ျပန္ေပးလိုက္ပါမယ္။ စဥ္းစားရတာ ရႈပ္သြားလား မသိဘူး :) ကဲ စဥ္းစားမေနဘဲ တခါတည္း ေရးၾကည့္ရေအာင္ပါ။

Function ကို ေခၚဖို႔အတြက္ GOTO op-code အစား call ဆိုတဲ့ သတ္သတ္ op-code တခုေဆာက္လိုက္ပါ။ သူလဲ GOTO လိုဘဲ သြားမဲ့ Line number ကို operand 0 ကေနယူပါမယ္။ GOTO နဲ႔ကြာတာက ေစာေစာက ေျပာသလို လက္ရွိ line number ကို stack မွာအရင္သြား save လုပ္ထားပါမယ္။

ၿပီးရင္ return ဆိုတဲ့ op-code အသစ္ထပ္ထည့္ပါအုံးမယ္။ သူက ခုနက stack ေပၚမွာ save လုပ္ထားတဲ့ line number ကိုျပန္သြားဖတ္ၿပီး အဲဒီ Line number ကိုေခၚမွာ ျဖစ္ပါတယ္။ သူလဲ GOTO နဲ႔အတူတူပါဘဲ။ သြားရမဲ့ line number ကို operand ကမယူဘဲ stack ေပၚကယူပါမယ္။ ဒီ op-code ႏွစ္ခုရွိရင္ function ေရးလို႔ရပါၿပီ။ ေရးၾကည့္ရေအာင္။
enum OpCode{    .......................
    .......................

    OP_CALL,        // call function

    OP_RETURN,        // return from function

    OP_END

};
ၿပီးရင္ Program class က လက္ရွိ line number variable ကို ဖတ္မဲ့ function ေရးဖို႔လိုပါတယ္။ ဒါမွ ဒီေကာင့္ကို Virtual Machine class ကေနဖတ္လို႔ရမွာပါ။
class Program {public:
    vector<Instruction>        InstructionList;

    int                        nCurrent;

    .......................

    .......................

    int GetCurrentInstruction() {

        return nCurrent;

    }

};
ၿပီးရင္ call နဲ႔ return အတြက္ Virtual Machine ထဲက Run() function မွာသြားေရးပါမယ္။
void Run(Program& program) {    while (true)
    {

        Instruction inst = program.Next();

        switch (inst.code)

        {

        ....................

        ....................

        case OP_CALL:

            program.PushStack( program.GetCurrentInstruction() );

            program.SetInstruction( inst.operand[0] );

            break;

        case OP_RETURN:

            Data1 = program.PopStack();

            program.SetInstruction( program.PopStack() );

            program.PushStack( Data1 );

            break;

        }

    }

};
Call op-code အလုပ္လုပ္ပုံက ရွင္းပါတယ္။ ခုေရာက္ေနတဲ့ line number ကို stack ေပၚအရင္ သြားသိမ္းၿပီးမွာ operand 0 မွာေပးထားတဲ့ line number ကို GOTO အတိုင္းဘဲ သြားခိုင္းပါတယ္။

Return op-code မွာေတာ့ stack ေပၚက ပထမ value ကိုအရင္ pop လုပ္ၿပီးသိမ္းထားပါတယ္။ ဒါဟာ function ကေန return ျပန္မဲ့ data ျဖစ္ပါတယ္။ Stack ေပၚက သူ႕ေနာက္က data ကေတာ့ call op-code မွာသိမ္းထားတဲ့ return ျပန္ရမဲ့ Line number ျဖစ္ပါတယ္။ သူ႕ကို pop လုပ္ၿပီး Program ရဲ႕ current line number ကို set လုပ္လိုက္ပါတယ္။ ၿပီးရင္ ခုန save လုပ္ထားတဲ့ data ကို stack ေပၚျပန္တင္ ေပးလိုက္ပါတယ္။

ဒီေနရာမွာ သတိထားရမွာက function ကေန value တခုခုကို stack တင္ကိုတင္ေပးမွ ရပါမယ္။ ဆိုလိုတာက high-level language စကားနဲ႔ေျပာရရင္ ကၽြန္ေတာ္တို႕ language မွာ void function ေရးလို႔မရဘူးဆိုတဲ့ သေဘာပါ။ အနဲဆုံး return 0; ေတာ့ ျပန္ကိုျပန္ေပးရပါမယ္။

ကဲၿပီးရင္ ေအာက္က C code ကို testing အေနနဲ႔ေရးၾကည့္ပါမယ္။
int a, int b;int Add() {
    return a + b;

};

a = 10, b = 5;

cout  Add();

a = 45, b = 32;

cout  Add();
ဘာမွ ေထြေထြထူးထူး code မဟုတ္ပါဘူး။ Number ႏွစ္ခုေပါင္းတဲ့ function ပါ။ ဒါကိုသုံးၿပီး Function ကိုစမ္းၾကည့္ရေအာင္ပါ။
// a is address 0, b is address 4000 put_mem    0    10
001 put_mem    4    5

002 call

003 num

004 put_mem    0    45

005 put_mem    4    32

006 call

007 num

008 end

// function A()

009 push_stack    0

010 push_stack    4

011 add

012 return
ဒီေနရာမွာ function ရဲ႕ input ကို global variable ကေနယူထားတာကို သတိထားမိလား မသိဘူး။ ဘာလို႔လဲဆိုေတာ့ ကၽြန္ေတာ္တို႔ function က parameter ေတြလက္မခံေသးပါဘူး။ ဒါေၾကာင့္ parameter နဲ႔ local variables ေတြလက္ခံေအာင္ေရးၾကည့္ရေအာင္ပါ။

ကၽြန္ေတာ္တို႕ function ထဲကို parameter ေတြထည့္ဖို႔ Stack ကိုဘဲသုံးပါမယ္။ ပထမ၊ function ထဲကိုေပးခ်င္တဲ့ parameter ေတြကို stack ေပၚကို function မေခၚခင္အရင္ pass လုပ္ေပးလုိက္ပါမယ္။ ၿပီးရင္ function ထဲေရာက္မွ stack ေပၚက data ေတြကို Get Stack နဲ႔ Set Stack Op-code ေတြသုံးၿပီး ျပန္ေခၚသုံးပါမယ္။ ဒီအတိုင္း အေပၚက code ကို global variable မသုံးဘဲ parameter သုံးၿပီး ေရးၾကည့္ရေအာင္ပါ။
// a is address 0, b is address 4000 put_stack    10
001 put_stack    5

002 call         13

003 num

004 pop_stack

005 pop_stack

006 put_stack    45

007 put_stack    32

008 call         13

009 num

010 pop_stack

011 pop_stack

012 end

// function A()

013 get_stack    2

014 get_stack    1

015 add

016 return
ဒါဆိုရင္ ကၽြန္ေတာ္တို႕ function က parameter ေတြကို stack ကေနလက္ခံလို႔ရပါၿပီ။ ဒါေပမဲ့ stack ေပၚတင္ထားတဲ့ data ႏွစ္ခုကို (line number 004 နဲ႔ 005 မွာ) ျပန္ၿပီး pop လုပ္လိုက္ပါတယ္။ ဒီလို function တခုေခၚတိုင္း stack ေပၚအရင္ တင္ထားတဲ့ parameters ေတြကို pop လိုက္လုပ္ေနရရင္ မလိုအပ္ဘဲ code ေတြမ်ားေနႏိုင္ပါတယ္။ ေရးရတဲ့ programmer အတြက္လဲ မေကာင္းသလို op-code ေတြမ်ားေနရင္ run ရတာလဲ ေႏွးေနမွာပါ။ ဒီအတြက္ function ကေန return ျပန္ရင္ သူ႕အလိုအေလွ်ာက္ pop လုပ္ေပးလိုက္ပါမယ္။ ဒီလိုလုပ္ဖို႔ function ရဲ႕ scope ကိုသိဖို႔လိုပါလိမ့္မယ္။

Note: Scope ဆိုတာ stack ေပၚမွာ ရွိတဲ့ function နဲ႔ဘဲ သက္ဆိုင္တဲ့ data ေတြကို ေခၚပါတယ္။ function အတြက္ stack ေပၚကို pass လုပ္ေပးလိုက္တဲ့ parameter ေတြ၊ function ထဲမွာ ရွိတဲ့ local variable ေတြဟာ function ရဲ႕ scope ေပါ့။ ဥပမာ၊ function ထဲမွာ local variable ၃ ခုရွိ္တယ္၊ parameter ၂ ခုလက္ခံတယ္ဆိုရင္ အဲဒီ function ရဲ႕ scope အရြယ္က ၅ ျဖစ္ပါတယ္။

ဒီေတာ့ function ရဲ႕ scope ကို သတ္မွတ္ေပးမဲ့ Op-code တခုထည့္လိုက္ပါမယ္။ သူ႕ကို OP_SCOPE လို႔ဘဲနာမည္ေပးထားပါမယ္။ set_stack တို႔၊ get_stack သုံးၿပီး stack ေပၚက data ေတြယူတဲ့အခါ stack ေပၚမွာလက္ရွိ တင္ထားတဲ့ data ေတြကို (function return address အပါအ၀င္) သိေနဖို႔လိုပါတယ္။ ဒီေတာ့ scope ထဲက data ေတြကိုဘဲဖတ္လို႔၊ ေရးလို႔ရမဲ့ သပ္သပ္ Op-code ေတြသတ္မွတ္မယ္။ ဒါဆို တျခား data ေတြနဲ႔မေရာေတာ့ဘဲ scope ေပၚက data ေတြကိုဘဲ ရွင္းရွင္းလင္းလင္း ဖတ္ႏိုင္တာေပါ့။ သူတို႔ကိုေတာ့ set_scope နဲ႔ get_scope လို႔ဘဲေခၚရေအာင္။ scope ရဲ႕ index (stack ရဲ႕ index မဟုတ္ပါ) ကို operand 0 ကဘဲယူပါမယ္။ ဒီ Op-code ၃ ခုနဲ႔ constant ကို set လုပ္ဖို႔ put_stack လို put_scope ကို program မွာေၾကျငာလိုက္ပါမယ္။
enum OpCode {    OP_TALK,
    ..........................

    ..........................

    OP_SCOPE,

    OP_SET_SCOPE,

    OP_GET_SCOPE,

    OP_PUT_SCOPE,

    OP_END

};
ၿပီးရင္ Program class မွာ scope ထဲက data ေတြကို သိမ္းဖို႔နဲ႔ ျပန္ဖတ္ဖို႔ function ႏွစ္ခုေရးပါမယ္။ ဒီ function ႏွစ္ခုက set_stack နဲ႔ get_stack တို႔နည္းတူ ေရးရင္ရပါတယ္။ ဒါေပမဲ့ retrieve လုပ္မဲ့ index ကိုလက္ရွိ scope offset နဲ႔ေပါင္းတြက္ၿပီးမွ return ျပန္ပါမယ္။
class Program {public:
    int         nScopeOffset;

    Program() : nCurrent(0), nScopeOffset(0) {

        pMemory = new unsigned char[10240];

        ZeroMemory(pMemory, 10240);

    };

    .........................

    .........................

    // set the data at specific scope index

    void SetScopeAt(int index, int val) {

        Stack[nScopeOffset - index] = val;

    };

    // retrieve the data at specific scope index

    int GetScopeAt(int index) {

        return Stack[nScopeOffset - index];

    };

};
အေပၚက code အလုပ္လုပ္ဖို႔ nScopeOffset ကို set လုပ္ဖို႔လိုပါတယ္။ ဒီေတာ့ Program class ထဲမွာ MakeScope() ဆိုတဲ့ function ကိုသြားေရးပါအုံးမယ္။ သူ႕လုပ္ငန္းကေတာ့ scope size ကိုအရင္ set လုပ္လုိက္မယ္။ ၿပီးရင္ လက္ရွိေရာက္ေနတဲ့ stack index ကို nScopeOffset ဆိုတဲ့ variable ထဲမွာမွတ္ထားပါမယ္။ ဒါဆို SetScopeAt() နဲ႔ GetScopeAt() ကိုသုံးတဲ့အခါက်ရင္၊ stack index က လက္ရွိ scope ကမွတ္ထားတဲ့ index ကေနဘဲ data ကိုယူသုံးမွာ ျဖစ္ပါတယ္။

ၿပီးရင္ MakeScope () function က မွတ္သြားတဲ့ scope ကိုျပန္ clear လုပ္တဲ့ function လဲေရးထားရပါမယ္။ ကဲေရးၾကည့္ရေအာင္ပါ၊
class Program {public:
    .........................

    .........................

    // make the scope and store the scope offset

    void MakeScope(int size) {

        int nPrevScopeOffset = nScopeOffset;

        nScopeOffset = Stack.size() – 1;

        PushStack(nPrevScopeOffset);

        PushStack(size);

    };

    // clear the scope made by MakeScope

    void ClearScope() {

        int nScopeSize = PopStack()

        nScopeOffset = PopStack();

        ClearStack(nScopeSize);

    };

};
ဒီေနရာမွာ ဘာေၾကာင့္ previous scope index နဲ႔ scope size ကို stack ေပၚမွာ သြားမွတ္ထားရသလဲ လို႔ေမးစရာရွိပါတယ္။ သူတို႕ကို သတ္သတ္ variable ထဲမွာ မွတ္ထားရင္ ရတာဘဲလို႔လဲ ေတြးမိေကာင္းေတြးမိမွာပါ။ ကၽြန္ေတာ္တို႔ function ဟာ တခါေခၚရင္ တႀကိမ္ဘဲ ေခၚမယ္ဆိုရင္ေတာ့ ဒီလို သတ္သတ္ variable ထဲသြားမွတ္ထားလို႔ရပါတယ္။ ဒါေပမဲ့ function ထဲကမွ ေနာက္ function တခုကို ထပ္ေခၚမယ္၊ ဒါမွမဟုတ္ recursion function ေတြကိုေခၚမယ္ဆိုရင္ အရင္ မွတ္ထားတဲ့ scope size ကိုေနာက္ေခၚတဲ့ function ရဲ႕ scope size က overwrite ျဖစ္သြားပါလိမ့္မယ္။ ဒီလိုကိစၥေတြမွာ stack ဟာ အရမ္းသင့္ေတာ္တဲ့ data structure ျဖစ္ပါတယ္။ Stack ေပၚမွာသြားသိမ္းေတာ့ overwrite မျဖစ္ေတာ့ဘဲ၊ ဒီ function ကျပန္ထြက္လို႔ ျပန္ Pop လုပ္လိုက္ရင္လဲ အရင္ function က scope size ကိုျပန္ရပါမယ္။

ခု၊ Program class ထဲက ေရးထားတဲ့ function ေတြနဲ႔ ကၽြန္ေတာ္တို႕ရဲ႕ op-code ေတြကိုတြဲေပးဖို႔ Virtual Machine ထဲက Run() function မွာေအာက္ကလို သြားေရးပါအုံးမယ္။
void Run(Program& program) {    while (true)
    {

        Instruction inst = program.Next();

        switch (inst.code)

        {

        ....................

        ....................

        case OP_SCOPE:

            program.MakeScope( inst.operand[0] );

            break;

        case OP_SET_SCOPE:

            program.SetScopeAt( inst.operand[0], program.PopStack() );

            break;

        case OP_GET_SCOPE:

            program.PushStack( program.GetScopeAt( inst.operand[0] ) );

            break;

        case OP_PUT_SCOPE:

            program.SetScopeAt( inst.operand[0], inst.operand[1] );

            break;

        }

    }

}
Return Op-code ထဲမွာလဲ လက္ရွိမွတ္ထားတဲ့ scope ကို clear လုပ္ဖို႔ ေအာက္ကလို သြားျပင္ပါအုံးမယ္။
case OP_RETURN:    Data1 = program.PopStack();
    program.SetInstruction( program.PopStack() );

    program.ClearScope();

    program.PushStack( Data1 );

    break;
program.ClearScope() ဆိုတဲ့ function ကို return address ကို Pop လုပ္ၿပီးနဲ႔ data1 ကို stack ေပၚျပန္မတင္ခင္ ေခၚရပါမယ္။ ဒါမွ ၾကားထဲက scope မွတ္ထားတဲ့ data ေတြအကုန္ clear လုပ္ၿပီးသားျဖစ္ၿပီး၊ data1 ကိုျပန္တင္တဲ့အခါ နဂိုအတုိင္းျဖစ္မွာပါ။ ၿပီးရင္ assembler အတြက္ AssemblyCodeTable ထဲမွာ "scope", "get_scope", "set_scope" နဲ႔ "put_scope" ေတြအတြက္ entry ေတြသြားေၾကျငာဖို႔လဲ မေမ့ပါနဲ႔။ ဒါမွ Assembler ကအသစ္ထဲ့ထားတဲ့ Op-code ေတြကို compile လုပ္ႏိုင္မွာ မဟုတ္လား။

ဒါဆိုကၽြန္ေတာ္တို႔ရဲ႕ script ေလးကို parameter သုံးၿပီး ျပန္ေရးၾကည့္ရေအာင္ပါ၊
000 put_stack    10001 put_stack    5
002 scope        2

003 call         11

004 num

005 put_stack    45

006 put_stack    32

007 scope        2

008 call         11

009 num

010 end

// function A()

011 get_scope    0

012 get_scope    1

013 add

014 return
ဒါဆို ကၽြန္ေတာ္တို႔ရဲ႕ language က function ကို parameter ေတြနဲ႔ေကာင္းေကာင္း support လုပ္ေပးႏိုင္တဲ့ full-flag language တခုနီးပါးျဖစ္လာပါၿပီ။ Recursive function ေတြလဲလက္ခံႏိုင္ပါတယ္။ Recursive function အလုပ္၊ လုပ္မလုပ္ testing စမ္းၾကည့္ရေအာင္။ Recursion အတြက္ နာမည္ႀကီးတြက္နည္း factorial ကိုတြက္ၾကည့္ရေအာင္။ C နဲ႔ဆို ေအာက္ကလို႔ coding ျဖစ္မွာေပါ့။
int factorial (int num) {    if (num==1)
        return 1;

    return factorial(num-1) * num;

}
ကၽြန္ေတာ္တို႔ language မွာေရးၾကည့္ပါမယ္၊
// call function factorial (address 00) with parameter value 8000 put_stack    8
001 scope        1

002 call         5

003 num

004 end

// function factorial()

// if (num==1) return 1

005 get_scope    0

006 put_stack    1

007 equal

008 ifn_goto     11

009 put_stack    1

010 return

// do (num-1)

011 get_scope    0

012 put_stack    1

013 subtract

// call factorial(num-1)

014 scope        1

015 call         5

// return factorial() * num

016 get_scope    0

017 multiply

018 return
 
Posted by Myint Kyaw Thu 

No comments:

Post a Comment

Related Posts Plugin for WordPress, Blogger...

အေထြးေထြးနည္းပညာမ်ား