c++ 아두이노 언어를 c#으로 변환해주는 인터프리터
이 인터프리터를 가지고 c++를 c#으로 전환해주고, 유니티에서 가상 시뮬레이션을 돌려준다.
먼저, runcode 함수를 실행 → 이때 lexer와 parser를 생성해주는 데 lexer에 전체 코드를 넣어준다. lexer와 parser의 역할은 뒷 내용을 보다보면 이해가 가겠지만, lexer는 코드의 string byte를 token으로 인식하게 해주고, parser는 이 token으로 해석을 시작한다. 이과정에서 AST(추상구문트리)의 노드 구조를 형성하고, 이후에 evaluater에서 이러한 트리구조를 실행해준다.
private static async UniTask RunCode(string code) {
var lexer = new Lexer(code, tokenizer);
var parser = new Parser(lexer, linker);
try {
var program = parser.ParseProgram();
if (parser.Errors().Count != 0) {
PrintParseErrors(parser.Errors());
return false;
}
var result = await evaluator.Evaluate(program, environment);
if (result.GetType() == ObjectType.ERROR_OBJ) {
Debug.LogError(((Error)result).message);
return false;
}
} catch (Exception e) {
Debug.LogError(e.Message);
return false;
}
return true;
}
lexer를 생성할 때, 전체의 코드와 static 멤버로 선언된 tokenizer를 lexer 생성자에 넣어주고, ReadChar라는 lexer의 메소드를 한번 실행 시켜준다. 이때, code의 전체 string 값을 인덱스로 접근하기 때문에, readposition이라는 다음 char, 즉 code의 맨처음 string 값의 다음을 읽을 준비를 해놓는다. (position → lexer에서의 현재 인덱스, readposition → 다음 code의 string byte를 가져오는 역할의 인덱스) lexer의 내부코드를 보면 알겠지만, nextToken이라는 함수 안에서 string byte들의 조합을 토큰으로 인식하는 역할을 한다. int이면 ‘i’,’n’,’t’로 읽힐 텐데 이를 Identifier로 인식하고 tokenizer에서 미리 저정의된 tokentype으로 만들어 준다
public Lexer(string code,Tokenizertokenizer){
this.code = code;
this.tokenizer = tokenizer;
ReadChar();
}
private void ReadChar() {
if (readPosition >= code.Length) {
currentCharAsByte = 0;
} else {
currentCharAsByte = (byte)code[readPosition];
}
position = readPosition;
readPosition += 1;
}
public Token NextToken() {
Token token;
SkipWhiteSpace();
switch ((char)currentCharAsByte) {
default:
if (IsLetter(currentCharAsByte)) {
var literal = ReadIdentifier();
if (literal.Equals("unsigned")) {
SkipWhiteSpace();
literal += " " + ReadIdentifier();
}
var type = tokenizer.LookupIdent(literal);
if (Regex.IsMatch(literal, @"^B+\d{8}$")) {
return new Token(TokenType.BYTE, literal);
}
return new Token(type, literal);
} else if (IsDigit(currentCharAsByte)) {
var tuple = ReadNumber();
return new Token(tuple.Item1, tuple.Item2);
} else {
token = new Token(TokenType.ILLEGAL, (char)currentCharAsByte + "");
}
break;
ReadChar();
return token;
}
parser를 생성할때, lexer와 static 멤버로 선언된 linker를 parser 생성자에 넣어준다.(linker란? 외부 환경, 즉 라이브러리를 연결해주는 역할) 생성자에서 토큰 타입에 따라, parse 해주는 미리 정의된 메소드들을 prefixParseFunctions 혹은 infixParseFunctions 에 넣어주고, currentToken의 tokenType에 따라 함수를 불러온다. infixParseFunctions[peekToken.type] 이런식으로. 또한 생성자에서 NextToken을 두번 불러주는 데, 이때 하나의 token 키워드가 될때까지 readchar를 실행 시켜주고, tokenizer에서 미리정의된 token값을 불러 오기위한 lexer.nextToken()을 실행시켜준다.
private void NextToken() {
currentToken = peekToken;
peekToken = lexer.NextToken();
}
그다음 parser에서 ParseProgram을 실행시켜주는데, 전체 코드를 program으로 변환하는 작업. program은 statement의 list로 이루어진 구조체이다. statement란, 전체코드에 대한 영역(모호한 개념인데, 뒤의 설명을 보면 조금더 이해하기 쉬울수 있다.)을 나타낸다.
- 현재 6가지의 statement 종류가 있는 데, init/assign/return/break/expression/block 이렇게 있는 데, 종류에 따라 느낌이 다르다. 각각 구성요소가 다른데, 예를 들어 init인 경우, 현재 토큰을 statement의 토큰값으로 설정한 initstatement를 반환하여 넘겨준다. 현재토큰이 Bool/float/double 등등 인 경우에 initstatements를 반환 받습니다. 이때 안에서 init data 함수가 실행 되는 데, 다음 토큰(PeekToken 결과값) 에 따라 어떤 parse를 돌릴지 결정이 됩니다. 예를 들어, 왼쪽중괄호’(’ 인경우 즉, TokenType.LPAREN 인 경우 functional literal로 parse함수를 실행 시켜줍니다.
privateStatementParseStatement(){
switch (currentToken.type) {
caseTokenType.BOOL:
caseTokenType.FLOAT:
caseTokenType.DOUBLE:
caseTokenType.CHAR:
caseTokenType.BYTE:
caseTokenType.SHORT:
caseTokenType.INT:
caseTokenType.LONG:
caseTokenType.UCHAR:
caseTokenType.USHORT:
caseTokenType.UINT:
caseTokenType.ULONG:
caseTokenType.STRING:
caseTokenType.VOID:
caseTokenType.CLASS:
return ParseInitDataStatement();
caseTokenType.CONST:
return ParseConstInitDataStatement();
caseTokenType.RETURN:
return ParseReturnStatement();
caseTokenType.BREAK:
return ParseBreakStatement();
caseTokenType.IDENT:
if (IsPeekToken(TokenType.ASSIGN))
return ParseAssignDataStatement();
return ParseExpressionStatement();
caseTokenType.SHARP:
return ParseSharpStatement();
caseTokenType.COMMA:
return ParseCommaStatement();
default:
return ParseExpressionStatement();
}
}
2. 이렇게 statements들의 리스트로 돌아가게 되는 데, 각각의 statements들을 parse 해줄때, statements들의 필드들(expression, List<statement>, token 등)을 설정해주는 데, 위의 단계에서 예시로든 init의 그런 예이다. expression이라는 필드(노드)가 있는 데, 개념이 조금 복잡하다. 일단 종류를 살펴보자면, bool/if/infix/postfix 등 엄청 많다. 각각의 expression별로 개념도 상당히 다른데, 일단 identifier종류를 살펴보면, token의 literal값, value값이 존재한다. 이런 숫자형이나 다른 문자형 string 같은 경우는 parseLiteral함수를 실행시켜 expression에 token과 value를 넣고 그 expression을 반환한다.
privateExpressionParseArrayLiteral(){
var array = newArrayLiteral{
token = currentToken,
elements = ParseExpressionList(TokenType.RBRACKET).ToArray()
};
return array;
}
여기까지가, parser의 parseprogram 단계이다. 요약하자면, string byte덩어리인 code를 lexer에서 string byte를 기준에 맞춰서 하나의 literal로 만들어주고 그 literal과 맞는 tokentype을 tokenizer에서 찾아와 literal과 tokentype을 가지고 있는 토큰으로 만들어준다. 그럼 이 토큰들을 가지고 parser에서 statements들로 꾸며준다. 그과정에서 현재 토큰에 따라 어떤 statement는 init이되고 어떤 statement는 block이 된다. 그 statement 내부에서 다음 토큰의 종류에 따라 실행되는 다른 parsing method가 실행되고, 그때, expression/List<statements>등 다양한 구성요소들로 구분한다. 이 파싱 단계에서 literal expression 으로 혹은 functional expression으로 종류가 나뉘며, 그토큰의 value를 설정해준다. value는 expression이 될수도 있고, byte가 될수도 있고, 여러가지이다.
이제 evaluator에서 evaluateprogram 메소드를 실행 시켜준다. 이제 parser에서 설정해준 statements/expression 들로 evaluate를 해준다. 위에서 parsing 해주는 단계와 각각의 영역들(program, expression, statement)라는 노드들의 트리구조를 실행시켜준다. (위의 단계는 트리구조를 설정) 그래서, literal을 해석 하던, callexpression이라는 게 나와서 hostfunction을 실행(ApplyFuncion()) 시켜주던 동작을 이단계에서 하게 된다.
public bool LinkLibrary(string libraryName){
var library = GetLibrary(libraryName);
if (library == null) {
return false;
}
library.Initialize();
evaluator.AddLibrary(library);
tokenizer.AddHeader(library);
return true;
}
추가적인 영역이 있다. 앞서 언급된 environment이다.(linker에서 라이브러리로 추가되는 영역) 이 environment는 parser에서 includestatement를 파싱해줄때, initialize가 된다.(라이브러리를 포함하는 include <Arduino.h> 같은 구문) 여기서 라이브러리에서 정의한 외부 클래스와 함수들을 가져다 쓸수 있게 되는 것이다. linker에서 initialize 해줄때, 각각의 라이브러리에서 함수 declare, header추가, field 추가 등의 작업을 마무리하여 evaluator가 evaluate을 할 때, 외부 클래스인지 혹은 외부 함수 인지 판단하여 실행시킨다.
public override void Initialize() {
Header.Add("Test", newHostFunction{
function = Test,
fieldDataTypes = newList<List<ObjectType>> {
newList<ObjectType>
{ObjectType.INT_OBJ,ObjectType.INT_OBJ,ObjectType.INT_OBJ}
}
});
Fields.Add("TestVariable1", newInt{value = 1});
Fields.Add("TestVariable2", newInt{value = 14});
}
private asyncUniTask<Object> Test(List<Object> args) {
var testClass = newClass{env = newEnvironment()};
testClass.env.Declare("begin", newHostFunction{
function = Begin,
fieldDataTypes = newList<List<ObjectType>> {
newList<ObjectType> { }
}
},
ObjectType.VOID_OBJ);
testClass.env.Declare("clear", newHostFunction{
function = Clear,
fieldDataTypes = newList<List<ObjectType>> {
newList<ObjectType> { }
}
},
ObjectType.VOID_OBJ);
testClass.env.Declare("show", newHostFunction{
function = Show,
fieldDataTypes = newList<List<ObjectType>> {
newList<ObjectType> { }
}
},
ObjectType.VOID_OBJ);
}
'Programming > C#' 카테고리의 다른 글
비동기 프로그래밍 - aync void를 지양하자 + (UniTaskVoid는?) (0) | 2023.12.01 |
---|---|
C# - 부동 소수점 오차 (0) | 2023.08.20 |
Effective C# - item 1 ~ 10 (0) | 2022.10.03 |
Effective C# - 제네릭의 활용) 타입 매겨변수가 IDispoable을 구현할 경우를 대비해 제네릭을 만든다. (0) | 2022.09.11 |
Effective C# - 제네릭의 활용) 런타임에 타입을 확인하여 최적의 알고리즘을 사용하자. (0) | 2022.09.11 |