프로그래밍 언어는 함수를 어떻게 정의합니까?


28

프로그래밍 언어는 어떻게 함수 / 방법을 정의하고 저장합니까? Ruby로 해석 된 프로그래밍 언어를 만들고 함수 선언을 구현하는 방법을 알아 내려고 노력 중입니다.

첫 번째 아이디어는 선언의 내용을지도에 저장하는 것입니다. 예를 들어

def a() {
    callSomething();
    x += 5;
}

그런 다음 맵에 항목을 추가합니다.

{
    'a' => 'callSomething(); x += 5;'
}

이것에 대한 문제 parse는 문자열에서 내 메소드 를 호출해야하기 때문에 재귀 적으로 parse발생 doSomething한다는 것입니다.

그렇다면 해석 된 언어는 어떻게 이것을 처리합니까?


아, 그리고 이것은 Programmers.SE에 대한 첫 번째 글이므로, 내가 잘못하고 있거나 주제가 아닌 경우 알려주십시오. :)
Doorknob

과거에는 토큰 내에 모든 인라인을 저장했으며 함수 호출은 어셈블리의 레이블과 같이 특정 오프셋으로 이동합니다. 스크립트를 토큰 화하고 있습니까? 아니면 매번 문자열을 구문 분석합니까?
Simon Whitehead

@SimonWhitehead 문자열을 토큰으로 나누고 각 토큰을 개별적으로 구문 분석합니다.
Doorknob

3
프로그래밍 언어 설계 및 구현에 익숙하지 않은 경우 주제에 대한 일부 문헌을 확인하고자 할 수 있습니다. 가장 인기있는 것은 "Dragon Book": en.wikipedia.org/wiki/… 이지만, 매우 간결한 다른 텍스트도 있습니다. 예를 들어, Aarne Ranta의 프로그래밍 언어 구현은 무료로 얻을 수 있습니다 ( bit.ly/15CF6gC) .
evilcandybag

1
@ ddyer 감사합니다! 다른 언어로 된 리스프 통역사를 검색했으며 실제로 도움이되었습니다. :)
Doorknob

답변:


31

"구문 분석"기능이 코드를 구문 분석 할뿐만 아니라 동시에 실행한다고 가정하면 정확합니까? 그렇게하려면 맵에 함수의 내용을 저장하는 대신 함수의 위치 를 저장하십시오 .

그러나 더 좋은 방법이 있습니다. 약간의 노력이 선행되지만 복잡성이 증가함에 따라 훨씬 더 나은 결과를 얻을 수 있습니다. 추상 구문 트리를 사용하십시오.

기본 아이디어는 코드를 한 번만 구문 분석하는 것입니다. 그런 다음 연산과 값을 나타내는 데이터 형식 집합이 있으며 다음과 같이 트리를 만듭니다.

def a() {
    callSomething();
    x += 5;
}

된다 :

Function Definition: [
   Name: a
   ParamList: []
   Code:[
      Call Operation: [
         Routine: callSomething
         ParamList: []
      ]
      Increment Operation: [
         Operand: x
         Value: 5
      ]
   ]
]

(이것은 가상 AST의 구조를 텍스트로 표현한 것입니다. 실제 트리는 텍스트 형식이 아닐 것입니다.) 어쨌든 코드를 AST로 파싱 한 다음 AST를 통해 통역사를 직접 실행해야합니다. 또는 두 번째 ( "코드 생성") 패스를 사용하여 AST를 일부 출력 형식으로 전환하십시오.

언어의 경우, 아마도 함수 이름 대신 함수 문자열에 함수 이름을 함수 AST에 매핑하는 맵이있을 것입니다.


좋아, 그러나 문제는 여전히 존재합니다. 재귀를 사용합니다. 이렇게하면 결국 스택 공간이 부족합니다.
Doorknob

3
@Doorknob : 특히 재귀를 사용하는 것은 무엇입니까? 모든 블록 구조 프로그래밍 언어 ( ASM보다 높은 수준의 모든 현대 언어)는 본질적으로 트리 기반이므로 재귀 적입니다. 스택 오버플로에 대해 어떤 특정 측면이 걱정됩니까?
메이슨 휠러

1
@Doorknob : 예, 머신 코드로 컴파일 된 경우에도 모든 언어의 고유 속성입니다. (콜 스택은이 동작을 나타냅니다.) 저는 실제로 설명 된 방식으로 작동하는 스크립트 시스템에 기여합니다. chat.stackexchange.com/rooms/10470/… 에서 채팅에 참여하십시오. 효율적인 해석과 스택 크기에 미치는 영향을 최소화하는 몇 가지 기술에 대해 설명하겠습니다. :)
Mason Wheeler

2
@Doorknob : AST의 함수 호출이 함수를 이름으로 참조하고 있기 때문에 재귀 문제 가 없습니다 . 실제 함수에 대한 참조가 필요하지 않습니다 . 머신 코드로 컴파일하는 경우 결국에는 함수 주소가 필요하므로 대부분의 컴파일러가 여러 번 패스합니다. 원 패스 컴파일러 를 원한다면 컴파일러가 미리 주소를 할당 할 수 있도록 모든 함수의 "전달 선언"이 필요합니다. 바이트 코드 컴파일러는 이것을 방해하지 않으며 지터는 이름 조회를 처리합니다.
Aaronaught

5
@Doorknob : 실제로 재귀 적입니다. 예, 스택에 16 개의 항목 만 있으면 구문 분석에 실패합니다 (((((((((((((((( x ))))))))))))))))). 실제로 스택은 훨씬 더 클 수 있으며 실제 코드의 문법적 복잡성은 상당히 제한적입니다. 확실히 그 코드가 사람이 읽을 수 있어야하는 경우.
MSalters

4

당신은 볼 때 구문 분석을 호출해서는 안됩니다 callSomething()(나는 당신이 callSomething아닌 의미한다고 가정합니다 doSomething). 차이 acallSomething다른 메소드 호출 중에 하나 인 방법이 정의된다.

새 정의가 표시되면 해당 정의를 추가 할 수 있는지 확인하는 것과 관련된 검사를 수행해야합니다.

  • 동일한 서명으로 기능이 존재하지 않는지 확인
  • 메소드 선언이 올바른 범위에서 수행되고 있는지 확인하십시오 (즉, 메소드를 다른 메소드 선언 내에 선언 할 수 있습니까?).

이러한 검사가 통과되었다고 가정하면이를지도에 추가하고 해당 방법의 내용을 확인할 수 있습니다.

와 같은 메소드 호출을 찾으면 callSomething()다음 점검을 수행해야합니다.

  • callSomething지도에 존재 합니까 ?
  • 제대로 호출 되었습니까 (발견 된 서명과 일치하는 인수 수)?
  • 인수가 유효합니까 (변수 이름을 사용하는 경우 선언 되었습니까?이 범위에서 액세스 할 수 있습니까?)?
  • 어디에서 callSomething을 호출 할 수 있습니까 (비공개, 공개, 보호)?

만약 당신 callSomething()이 괜찮다면,이 시점에서 당신이하고 싶은 것은 실제로 당신이 그것에 접근하고자하는 방법에 달려 있습니다. 엄밀히 말하면,이 시점에서 그러한 호출이 괜찮다는 것을 알고 나면 추가 세부 사항으로 가지 않고 메소드 이름과 인수 만 저장할 수 있습니다. 프로그램을 실행할 때 런타임에 있어야하는 인수를 사용하여 메소드를 호출합니다.

더 나아가고 싶다면 문자열뿐만 아니라 실제 방법에 대한 링크를 저장할 수 있습니다. 이것은 더 효율적이지만 메모리를 관리해야하는 경우 혼동 될 수 있습니다. 처음에는 끈을 붙잡고있는 것이 좋습니다. 나중에 최적화를 시도 할 수 있습니다.

참고이하는 모든 당신이 당신의 프로그램에 모든 토큰을 인식하고 그들이 알고있는 수단 프로그램, lexxed 한 가정입니다 됩니다 . 그것들이 아직 서로 이해가되는지 아는 것은 아닙니다. 파싱 단계입니다. 아직 토큰이 무엇인지 모르는 경우 먼저 해당 정보를 얻는 데 중점을 두십시오.

도움이 되길 바랍니다. 프로그래머 SE에 오신 것을 환영합니다!


2

귀하의 게시물을 읽으면서 귀하의 질문에 두 가지 질문이 있음을 발견했습니다. 가장 중요한 것은 파싱하는 방법입니다. (명시 적 또는 암시 적) 문법으로 텍스트 프로그램을 "재귀 적으로"순회하기 위해 사용할 수 있는 많은 종류의 파서 (예 : 재귀 하강 파서 , LR 파서 , Packrat 파서 ) 및 파서 생성기 (예 : GNU bison , ANTLR )가 있습니다.

두 번째 질문은 함수의 저장 형식에 관한 것입니다. 구문 지향 변환을 수행하지 않을 때는 프로그램의 중간 표현을 작성하십시오.이 구문은 추가 구문 처리 (컴파일, 변환, 실행, 쓰기)를 위해 추상 구문 트리 또는 일부 사용자 정의 중간 언어 일 수 있습니다 . 파일 등).


1

일반적인 관점에서 함수의 정의는 코드에서 레이블 또는 책갈피에 지나지 않습니다. 대부분의 다른 루프, 범위 및 조건 연산자는 비슷합니다. 하위 레벨의 추상화에서 기본 "점프"또는 "고토"명령을 나타냅니다. 함수 호출은 기본적으로 다음과 같은 저수준 컴퓨터 명령으로 요약됩니다.

  • 모든 매개 변수의 데이터와 현재 함수의 다음 명령어에 대한 포인터를 "호출 스택 프레임"으로 알려진 구조로 연결합니다.
  • 이 프레임을 호출 스택으로 밀어 넣습니다.
  • 함수 코드의 첫 번째 줄의 메모리 오프셋으로 이동하십시오.

"반환"문구 또는 이와 유사한 문구는 다음을 수행합니다.

  • 레지스터로 반환 할 값을로드하십시오.
  • 호출자에 대한 포인터를 레지스터에로드하십시오.
  • 현재 스택 프레임을 팝합니다.
  • 발신자의 포인터로 이동합니다.

따라서 함수는 더 높은 수준의 언어 사양에서 추상화 된 것이므로 사람이 코드를보다 관리하기 쉽고 직관적으로 구성 할 수 있습니다. 어셈블리 또는 중간 언어 (JIL, MSIL, ILX)로 컴파일되고 기계 코드로 렌더링 될 때 거의 모든 추상화가 사라집니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.