Pages

Friday 7 September 2012

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

Adding Operands

ခုေလာေလာဆယ္ ကၽြန္ေတာ္တို႕ရဲ႕ Op-code TALK က ပုံေသတမ်ိဳးဘဲ print ထုတ္ေပးမွာ ျဖစ္ပါတယ္။ ဒီေတာ့ ပုံေသမဟုတ္ဘဲ ေပးထားတဲ့ number တခု screen ေပၚကိုရိုက္ထုတ္ေပးတဲ့ Op-code အသစ္တခု ထပ္ ထည့္ၾကည့္ရေအာင္။

ဒီလို user (ကိုယ့္ language ကိုသုံးမဲ့ programmer) ကေပးတဲ့ data ကို print ထုတ္ျပဖို႔အတြက္ Op-code ကို data နဲ႔တြဲေပးဖို႔လိုပါတယ္။ အဲလို Data မိ်ဳးကို operand လို႔ေခၚတာ သိၾကမွာပါ။ ဒီေနရာမွာ ကိုယ့္ code မွာ operand ဘယ္ႏွစ္ခုသုံးခြင့္ေပးမလဲ ဆိုတာ ဆုံးျဖတ္ဖို႔လိုပါတယ္။ ဒီ tutorial မွာေတာ့ operand ၂ ခုလက္ခံေအာင္ ေရးထားပါတယ္။ ၂ ခုဆို assembly အတြက္ေတာ့ ေတာ္ေတာ္အလုပ္ျဖစ္ေနပါၿပီ။

ကဲ operand နဲ႔ Op-code တြဲေပးဖို႔ ေအာက္ကလို code တခ်ိဳ႕ျပင္ပါမယ္။
class Instruction {public:
    OpCode    code;

    int       operand[2];

    Instruction(OpCode _c, int _p1, int _p2) : code(_c) {

        operand[0] = _p1;

        operand[1] = _p2;

    };

};
NUM ဆိုတဲ့ Op-code အသစ္တခုထပ္ထည့္ရေအာင္၊
enum OpCode {    OP_TALK,
    OP_NUM,

    OP_END,

};
Virtual Machine ထဲက switch ထဲမွာလဲ NUM ကို process လုပ္တဲ့ code ျဖည့္ထည့္ရပါမယ္၊
void Run(Program& program) {    while (true)
    {

        Instruction inst = program.Next();

        switch (inst.code)

        {

        case OP_TALK:

            printf("Hello, I am simplest language!\n");

        break;

        case OP_NUM:

            printf("Current Number: %d", inst.operand[0]);

            break;

        case OP_END:

            return;

        }

    }

};
ကဲဒါဆို ကၽြန္ေတာ္တို႔ရဲ႕ code အသစ္ေလးကို စမ္းၾကည့္ရေအာင္။

void main() {
    Program MyProgram;
    // my script :)

    MyProgram.Add(Instruction(OP_TALK, 0, 0));

    MyProgram.Add(Instruction(OP_NUM, 101, 0));

    MyProgram.Add(Instruction(OP_NUM, 456, 0));

    MyProgram.Add(Instruction(OP_END, 0, 0));

    vm.Run(MyProgram);

};
ဒါဆို ကၽြန္ေတာ္တို႔ရဲ႕ script ကေအာက္ကလို print ထုတ္ေပးပါလိမ့္မယ္။

Hello, I am simplest language!
Current Number: 101
Current Number: 456

Adding Memory Support

Op-code တခုခ်င္းဆီမွာ data ကို operand အျဖစ္ထည့္ေပးလို႔ရတယ္။ ဒါေပမဲ့ Op-code တခုနဲ႔ တခုၾကား data ကို share လုပ္ခ်င္ရင္ ဘယ္လို လုပ္မလဲ။ global data pool တခုေတာ့ လိုေနပါတယ္။ ဒါမွ ပထမ Op-code က process လုပ္လို႔ထြက္လာတဲ့ data ကို ေနာက္ op-code က ယူသုံးလို႔ရမွာေလ။ ဒီအတြက္ Memory ဆိုတဲ့ array တခုကို Program class မွာသြားေၾကညာေပးရပါမယ္။

ဒီေနရာမွာ ေမးစရာက ဘာေၾကာင့္ global memory ကို Virtual Machine မွာမထည့္ဘဲ Program မွာသြားထည့္ရတာလဲ ဆိုတာပါ။ ဟုတ္တယ္ေလ၊ Global memory က Virtual Machine မွာအသုံးျပဳမွာ မဟုတ္လား။ ဒီလို ေၾကညာလဲ ရေတာ့ရပါတယ္။ ဒါေပမဲ့ Virtual Machine မွာ program ၂ ခုထက္ပို run တဲ့ အခါ ပထမ program သုံးသြားတဲ့ memory ကိုေနာက္ program က share သုံးရပါလိမ့္မယ္။ ဒီေတာ့ မလိုလားအပ္တဲ့ error ေတြတက္လာႏိုင္ပါတယ္။ ဒါေၾကာင့္ ကၽြန္ေတာ္တို႔က global memory ရဲ႕ scope ကို Program class level မွာဘဲထားၿပီး Program class တခုတိုင္း global memory pool တခုရွိေစခ်င္လို႕ global memory ကို Program class မွာေၾကညာရျခင္း ျဖစ္ပါတယ္။
class Program {public:
    vector<Instruction>    InstructionList;

    int                    nCurrent;

    unsigned char*         pMemory;

    ...........

    ...........

};
pMemory ကို unsigned char အေနနဲ႔ဘဲ ေၾကညာထားပါတယ္။ ဒီေနရာမွာ 4-bytes ရွိတဲ့ int အျဖစ္ေပးရင္ ရေပမဲ့ 1 byte ရွိတဲ့ char data type ကို save သြားလုပ္ရင္လဲ 4 bytes စာယူပါလိမ့္မယ္။ ဒါေၾကာင့္အငယ္ဆုံး size ကိုေပးထားတာပါ။ ဒီေတာ့ 4-byte ရွိတဲ့ integer တခု save လုပ္ခ်င္ရင္ pMemory မွာ ၄ ေနရာယူပါလိမ့္မယ္။

Memory ကိုအသုံးျပဳဖို႔ function တခ်ိဳ႔လုိပါလိမ့္မယ္။
class Program {public:
    ...........

    ...........

    // write data to specific memory address

    void WriteMem(int address, void* data, int size) {

        memcpy(pMemory + address, data, size);

    };

    // read data from the specific memory address

    void ReadMem(int address, void* data, int size) {

        memcpy(data, pMemory + address, size);

    };

    ...........

    ...........

    Program() : nCurrent(0) {

        pMemory = new unsigned char[10240];

        ZeroMemory(pMemory, 10240);

    };

    ~Program() {

        delete pMemory;

    }

};
WriteMem() ဆိုတဲ့ function က memory array ေပၚမွာ data သြားေရးတာပါ။ Address ကေတာ့ ကိုယ္ေရးခ်င္တဲ့ address ေပါ့။ ဥပမာ၊ ပထမ integer value တခုသြားေရးခ်င္ရင္၊

program.WriteMem(0, 101, 4); ေပါ့။

ဒုတိယ integer တခုသြားေရးခ်င္ရင္ address 4 မွာေရးရပါမယ္ (ပထမ integer က address 0 ကေန 3 ထိေနရာယူထားတယ္ေလ)။

program.WriteMem(4, 456, 4); ေပါ့။

ReadMem ကေတာ့ WriteMem ရဲ႕ေျပာင္းျပန္ memory ကေနျပန္ဖတ္တဲ့ function ပါ။ Program() constructor ထဲမွာ ကၽြန္ေတာ္တို႔ရဲ႕ memory ကို ေဆာက္ေပးဖို႔လိုပါလိမ့္မယ္။ ခုကၽြန္ေတာ္တို႕ memory size ကို 10KB ေပးထားပါတယ္။ ဒီေတာ့ ကၽြန္ေတာ္တို႕ language မွာ 10KB ထက္ပိုၿပီး သိမ္းလို႔မရဘူးေပါ့ဗ်ာ။ ဒီထက္ပိုသိမ္းခ်င္ရင္ memory size ကိုျပင္ေပးရပါလိမ့္မယ္။ Destructor ထဲမွာ memory ကို delete လုပ္ခဲ့ဖို႔လဲ မေမ့ပါနဲ႔။

ဒီေနရာမွာ အားသာခ်က္တခုက global memory ကို 0 ေတြတခါတည္း initialize လုပ္ထားလို႔ရတာပါဘဲ (constructor ထဲမွာ "ZeroMemory(pMemory, 10240);" လို႔ေရးထားတယ္ေလ)။ C++ မွာဆို memory ကအစမွာ ေျဗာက္ေသာက္ျဖစ္ေနတာမို႕ variable ေၾကညာတိုင္းအၿမဲ initialize လုပ္ေပးရပါတယ္။ ဒါက Java လို Virtual Machine မွာ run တဲ့ programming language ေတြရဲ႕အားသာခ်က္တခုပါ။

ခုကၽြန္ေတာ္တို႔ဆီမွာ global memory ရွိၿပီမို႔ memory ကို access လုပ္တဲ့ Op-code တခ်ိဳ႕ေရးဖို႕ဘဲက်န္ပါေတာ့တယ္။ ဒီအတြက္ memory ကိုထဲကိုသြားသိမ္းမဲ့ OP_PUT_MEM ဆိုတဲ့ Op-code ကိုထည့္လိုက္ပါ။
enum OpCode {    .......
    OP_PUT_MEM,    // write operand to global memory

    .......

};
ေနာက္ၿပီးရင္ VirtualMachine::Run() function ထဲမွာ OP_PUT_MEM အတြက္သြားေရးပါ။
Instruction inst = program.Next();switch (inst.code)
{

    ......

case OP_PUT_MEM:

    program.WriteMem(inst.operand[0], (void*)&(inst.operand[1]), 4);

    break;

    ......

}
ဒီေနရာမွာ Instruction ရဲ႕ ပထမ operand ကကိုယ္ေရးခ်င္တဲ့ memory address ျဖစ္ၿပီး ဒုတိယ operand ကကိုယ္သိမ္းခ်င္တဲ့ data ျဖစ္ပါတယ္။ သိမ္းတဲ့ data က integer မို႔ size ကေတာ့ 4 ပါ။ ၿပီးရင္ NUM ဆိုတဲ့ Op-code ကို operand ကို print လုပ္မဲ့အစား memory ထဲက data ကို print လုပ္တဲ့ function ေျပာင္းေရးၾကည့္ရေအာင္။
...........case OP_NUM:
    {       

        int data;

        program.ReadMem(inst.operand[0], (void*) &data, 4);

            printf("Current Number: %d", data);

    }

    break;

...........

...........
ဒီေနရာမွာ operand 0 က data မဟုတ္ေတာ့ဘဲ print ထုတ္မဲ့ memory ရဲ႕ address ျဖစ္သြားပါတယ္။ ကဲ ခုကၽြန္ေတာ္တို႕ရဲ႕ code ကို testing လုပ္ၾကည့္ရေအာင္။ main() function ကိုေအာက္ကအတိုင္းျပင္ပါမယ္။
void main() {    Program MyProgram;


    // my script :)

    MyProgram.Add(Instruction(OP_TALK, 0, 0));

    MyProgram.Add(Instruction(OP_PUT_MEM, 0, 101));

    MyProgram.Add(Instruction(OP_PUT_MEM, 4, 456));

    MyProgram.Add(Instruction(OP_NUM, 0, 0));

    MyProgram.Add(Instruction(OP_NUM, 4, 0));

    MyProgram.Add(Instruction(OP_END, 0, 0));

    vm.Run(MyProgram);

};
ဒါဆိုရင္ ေအာက္ကအတုိင္း output ရပါမယ္။ Output မွာသိပ္အေျပာင္းအလဲ မရွိေပမဲ့ ခုဆိုကၽြန္ေတာ္တို႕ရဲ႕ language ေလးက data ေတြကို memory မွာသြားသိမ္းလို႔ရေနပါၿပီ :)

Hello, I am simplest language!
Current Number: 101
Current Number: 456

Using Local Storage (Stack)

Global Memory ရွိတာေကာင္းပါတယ္။ ဒါေပမဲ့ global memory က တြက္ခ်က္ထားတဲ့ data ေတြကိုအၿမဲ (permanent) သိမ္းထားဖို႔အတြက္ဘဲ ေကာင္းပါတယ္။ တြက္ခ်က္တုံး ၾကားထဲက (intermediate) data ေတြကို သိမ္းဖို႔အတြက္ေတာ့ သိပ္အဆင္မေျပလွဘူး။ ဘာေၾကာင့္ဆိုတာ ဥပမာ ေပးပါမယ္။ 1 + (5 + 4) ဆိုတဲ့ equation ကိုတြက္ဖို႕အတြက္ 5 + 4 ကိုေပါင္းၿပီး global memory မွာ save လုပ္ရပါမယ္။ ၿပီးရင္ save လုပ္ထားတဲ့ value ကို 1 နဲ႔ထပ္ေပါင္းရပါတယ္။ ဒီအတြက္ 5 + 4 ကို save လုပ္ဖို႔ global memory တေနရာစာ ပုတ္ေနပါလိမ့္မယ္။ အတြက္အခ်က္ေတြသာ အမ်ားႀကီးလုပ္ရင္ မလိုအပ္ဘဲ intermediate data ေတြကို global memory မွာ သြားသြားၿပီး သိမ္းေနရပါမယ္။ ဒါေၾကာင့္ global memory အစား temporary ဘဲသိမ္းလို႔ရမဲ့ memory တမ်ိဳးလိုပါလိမ့္မယ္။ ဒီအတြက္ Stack ဆိုတဲ့ data structure ဟာ temporary data pool အျဖစ္သုံးဖို႔သင့္ေတာ္ပါတယ္။ ဘာေၾကာင့္ဆိုတာ ေနာက္ပိုင္း arithmetic ေတြသုံးတဲ့ အခါ ေတြ႕ပါလိမ့္မယ္။

Note: ဒီေနရာမွာ stack လို႔ေျပာလို႔ local variables ေတြသိမ္းတဲ့ function call stack ကိုသြားျမင္ေကာင္းျမင္ပါလိမ့္မယ္။ ဟုတ္ပါတယ္။ ခုကၽြန္ေတာ္တို႕သုံးမယ့္ stack ဟာ function call stack ပါဘဲ။ Local variables ေတြဟာ ဘာေၾကာင့္ local ျဖစ္ေနရသလဲဆိုတာကို stack ကို implement လုပ္ၿပီးရင္ သိသြားပါလိမ့္မယ္။

ဒီအတြက္ အရင္ global memory လိုဘဲ Program ဆိုတဲ့ class မွာ Stack ဆိုတဲ့ vector list တခုသြားေၾကညာပါ။ ၿပီးရင္ stack ကိုသုံးမဲ့ function အနည္းငယ္လဲ ထည့္ေပးဖို႔လိုပါမယ္။ ဒီေတာ့ ေအာက္ကလို stack ကို Program class မွာသြားေၾကညာပါ။
class Program {public:
    vector<Instruction>        InstructionList;

    int                        nCurrent;

    unsigned char*             pMemory;

    vector<int>                Stack;

    // push data on top of stack

    void PushStack(int val) {

        Stack.push_back(val);

    };

    // pop data from top of stack and return it's value

    int PopStack() {

        int val = Stack.back();

        Stack.pop_back();

        return val;

    };

    // return the top value of the stack without popping it

    int TopStack() {

        return Stack.back();

    };

    // clear the stack up to specific range

    void ClearStack(int range) {

        for (int i = 0; i < range; i++)

            Stack.pop_back();

    };

    // clear all the data on the stack

    void ClearAllStack() {

        Stack.clear();

    };

    // set the data at specific stack index

    void SetStackAt(int index, int val) {

        Stack[Stack.size() - index - 1] = val;

    };

    // retrieve the data at specific stack index

    int GetStackAt(int index) {

        return Stack[Stack.size() - index - 1];

    };

    // add range of data onto stack. This function is used to "grow" the stack

    // to specific size we want

    void AddStack(int range) {

        for (int i = 0; i < range; i++)

        Stack.push_back(0);

    }

    // retrieve the size of the stack

    int StackSize() {

        return Stack.size();

    };

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

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

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

};
Function ေတြမ်ားေပမဲ့၊ code ေတြက ဖတ္ရတာ လြယ္ပါတယ္။ အားလုံးက Stack ဆိုတဲ့ list ထဲ data ထည့္တာ၊ ထုတ္တာ လုပ္တဲ့ code ေတြပါဘဲ။ Function တခုတိုင္းကို ဘာလုပ္တယ္ဆိုတာ comment ေတြၾကည့္ၿပီးသိႏိုင္တာမို႔ အေသးစိတ္မရွင္းေတာ့ပါဘူး။ ဒါဆို ကၽြန္ေတာ္တို႕ program မွာ stack ဆိုတာရပါၿပီ။ အဲဒီ stack ကိုသုံးဖို႕ Op-code တခ်ိဳ႕ ေရးဖို႕ေတာ့ လိုပါလိမ့္မယ္။ ေရွေရွေ၀းေ၀း စဥ္းစားစရာ မလိုပါဘူး။ ခုနက stack ရဲ႕ function ေတြအတိုင္း Op-code ေတြကိုေရးလိုက္ရုံပါဘဲ။ ခု Stack ကိုသုံးဖို႔ Op-code အသစ္ေတြ ေအာက္ကလို ေၾကညာလိုက္ပါ။
enum OpCode {    .......
    OP_PUSH_STACK,        // for push stack function

    OP_POP_STACK,        // for pop stack function

    OP_GET_STACK,        // for get stack at function

    OP_PUT_STACK,        // for putting a value onto stack

    OP_SET_STACK,        // for set stack at function

    OP_CLEAR_STACK,        // for clear stack function

    OP_ADD_STACK,        // for add stack function

    .......

};
ကဲဒါဆို ဒီ Op-code ေတြအတြက္ Virtual Machine ရဲ႕ Run function ထဲက switch ထဲမွာ သြားေရးရေအာင္။ Push stack op-code အတြက္ေအာက္ကလိုေရးပါတယ္။
Instruction inst = program.Next();switch (inst.code)
{

    ......

case OP_PUSH_STACK:

    {

        int data;

        program.ReadMem(inst.operand[0], &data, 4);

        program.PushStack(data);

    }

    break;

    ......

}
OP_PUSH_STACK ဟာ operand 0 ထဲမွာေပးထားတဲ့ memory address အတုိင္းသြားဖတ္ၿပီး ရလာတဲ့ data ကို stack ေပၚမွာသြားသိမ္းမွာ ျဖစ္ပါတယ္။ ဒီေတာ့ OP_POP_STACK ကသူ႔ရဲ႕ေျပာင္းျပန္ေပါ့။ OP_POP_STACK က stack ေပၚက data ကို operand 0 ထဲမွာ ေပးထားတဲ့ address အတုိင္း memory ေပၚသြားသိမ္းမွာ မဟုတ္လား။
case OP_POP_STACK:{
    int data = program.PopStack();

    program.WriteMem(inst.operand[0], &data, 4);

}

break;
OP_GET_STACK နဲ႔ OP_SET_STACK ကေတာ့ stack ရဲ႕ index တခုခုထဲမွာရွိတဲ့ data ကို stack အေပၚမွာထပ္သြားသိမ္းမွာပါ။ သြားသိမ္းမဲ့ index ကိုေတာ့ operand 0 ကေရးေပးလိုက္မွာပါ။ ဒီေနရာမွာ သိမ္းရမဲ့ index ကို array လိုမဟုတ္ဘဲ ေနာက္ကေန ျပန္ၾကည့္ရမွာျဖစ္ပါတယ္။ ဥပမာ၊ index 0 က ေနာက္ဆုံး ထည့္ထားတဲ့ data ျဖစ္ပါတယ္။ ဒီေတာ့ အဲဒီႏွစ္ခုကို ေရးၾကည့္ရေအာင္၊
......case OP_GET_STACK:
    program.PushStack( program.GetStackAt(inst.operand[0]) );

break;

case OP_SET_STACK:

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

break;......
OP_PUT_STACK ကေတာ့ stack ေပၚကို data ကိုသူ႕ၾကည့္ဘဲ ထည့္ခ်င္ရင္ သုံးဖို႔အတြက္ပါ။ Data ကို operand 0 ကတိုက္ရိုက္ယူပါမယ္။ ကဲ ေရးၾကည့္ရေအာင္၊
......case OP_PUT_STACK:
    program.PushStack( inst.operand[0] );

break;

......
OP_CLEAR_STACK နဲ႕ OP_ADD_STACK ကေတာ့ Program class ရဲ႕ function ေတြျဖစ္တဲ့ AddStack() နဲ႕ ClearStack() ကိုတုိက္ရိုက္ေခၚသုံးဖို႕ပါ။ Parameters ေတြကို operand ေတြကဘဲယူပါမယ္။ ကဲေရးၾကည့္ရေအာင္၊
......case OP_ADD_STACK:
    program.AddStack( inst.operand[0] );

break;

case OP_CLEAR_STACK:

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

break;

......
ကဲဒါဆို stack နဲ႕ပတ္သက္တဲ့ Op-code ေတြအကုန္ေတာ့ ေရးၿပီးပါၿပီ။ Testing လုပ္ဖို႕ NUM ဆိုတဲ့ Op-code ကို memory ကတိုက္ရိုက္ Print ထုတ္မဲ့အစား stack ေပၚကေန print ထုတ္လို႕ရေအာင္ ျပင္ေရးရေအာင္။
...........case OP_NUM:
    printf("Current Number: %d", program.TopStack());

    break;

...........
ကဲဒါဆို testing လုပ္ဖို႕ main() function ထဲက ကၽြန္ေတာ္တို႕ရဲ႕ script ကို stack သုံးၿပီး ျပင္ေရးရေအာင္၊
void main() {    Program MyProgram;


    // my script :)

    MyProgram.Add(Instruction(OP_TALK, 0, 0));

    MyProgram.Add(Instruction(OP_PUT_MEM, 0, 101));

    MyProgram.Add(Instruction(OP_PUT_MEM, 4, 456));

    MyProgram.Add(Instruction(OP_PUSH_STACK, 0, 0));

    MyProgram.Add(Instruction(OP_PUSH_STACK, 4, 0));

    MyProgram.Add(Instruction(OP_NUM, 0, 0));

    MyProgram.Add(Instruction(OP_POP_STACK, 4, 0));

    MyProgram.Add(Instruction(OP_NUM, 0, 0));

    MyProgram.Add(Instruction(OP_CLEAR_STACK, 1, 0));

    MyProgram.Add(Instruction(OP_END, 0, 0));



    vm.Run(MyProgram);

};
Output ကေတာ့၊

Hello, I am simplest language!
Current Number: 456
Current Number: 101

ဒီေနရာမွာ memory address 4 (data က 456) ကို stack ေပၚေနာက္ဆုံး push လုပ္တဲ့အတြက္ အရင္ဆုံး print ထုတ္ျပတာျဖစ္ပါတယ္။ ေနာက္ pop လုပ္လိုက္ေတာ့ memory address 0 က data က်န္ပါတဲ့။ ဒါေၾကာင့္ သူ႔ data ျဖစ္တဲ့ဂဏန္း 101 ကို print လုပ္တာပါ။ ၿပီးရင္ stack ကို clear လုပ္လိုက္ပါတယ္။ အဲဒီေနာက္မွ NUM ကိုေခၚရင္ေတာ့ error တက္မွာပါ။ ဘာေၾကာင့္ဆို stack ေပၚမွာ data မရွိေတာ့တဲ့အတြက္ပါ။ ခုကၽြန္ေတာ္တို႕ language ေလးက တျဖည္းျဖည္းက်ယ္ျပန္႕လာသလို၊ limit ေတြလဲမ်ားလာပါတယ္။

No comments:

Post a Comment

Related Posts Plugin for WordPress, Blogger...

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