11

트릭을 수행하는 생성기를 이미 작성했지만 오프 쉐어 규칙을 구현하는 최선의 방법을 알고 싶습니다.오프사이드 규칙을 구현하는 방법에 대해 어떻게 생각하십니까?

잠깐 : Off-side rule은이 문맥에서 들여 쓰기가 구문 요소로 인식되는 것을 의미합니다. 여기

이 가능한 형태로 캡처 들여 쓰기, 나는 언어로 답을 제한하지 않는 것이 tokenizers을 만들기위한 의사의 오프사이드 규칙입니다 :

token NEWLINE 
    matches r"\n\ *" 
    increase line count 
    pick up and store the indentation level 
    remember to also record the current level of parenthesis 

procedure layout tokens 
    level = stack of indentation levels 
    push 0 to level 
    last_newline = none 
    per each token 
     if it is NEWLINE put it to last_newline and get next token 
     if last_newline contains something 
      extract new_level and parenthesis_count from last_newline 
      - if newline was inside parentheses, do nothing 
      - if new_level > level.top 
       push new_level to level 
       emit last_newline as INDENT token and clear last_newline 
      - if new_level == level.top 
       emit last_newline and clear last_newline 
      - otherwise 
       while new_level < level.top 
        pop from level 
        if new_level > level.top 
         freak out, indentation is broken. 
        emit last_newline as DEDENT token 
       clear last_newline 
     emit token 
    while level.top != 0 
     emit token as DEDENT token 
     pop from level 

comments are ignored before they are getting into the layouter 
layouter lies between a lexer and a parser 

개 이상의 NEWLINE를 생성하지 않습니다이 layouter 시간이 들리고 들여 쓰기가있을 때 NEWLINE을 생성하지 않습니다. 따라서 구문 분석 규칙은 매우 간단합니다. 꽤 좋은 생각이지만, 더 좋은 방법이 있다면 알려주십시오.

잠시 사용하는 동안, 나는 DEDENTs 이후에 어쨌든 newline을 내보내는 것이 좋을 수 있음을 알았습니다.이 방법은 표제를위한 트레일러로 들여 쓰기를 유지하면서 NEWLINE으로 표현을 분리 할 수 ​​있습니다.

답변

8

저는 지난 몇 년 동안 몇 개의 들여 쓰기 중심 도메인 특정 언어에 대한 토큰 화기 및 파서를 작성했으며, 여러분이 가지고있는 것은 가치있는 것이 무엇이든간에 저에게 상당히 합리적입니다. 실수가 아니라면, 여러분의 방법은 파이썬이하는 것과 아주 비슷합니다. 예를 들어 어떤 무게를 지니고있는 것처럼 보입니다.

파서를 치기 전에 NEWLINE NEWLINE INDENT를 방금 들여 보내기로 바꾸는 것은 올바른 방법으로 보입니다. 파서에서 항상 엿보는 것이 고통입니다. 나는 실제로 그 단계를 3 단계 과정으로 분리하여 별도의 레이어로 수행했습니다. 첫 번째 결합은 렉서와 레이아웃 작성자가 NEWLINE 룩어 헤드 항목을 빼고 (매우 단순하게 만들었습니다), 두 번째는 매우 간단했습니다.) 레이어는 연속 된 NEWLINE을 접고 NEWLINE INDENT를 단지 INDENT (또는 실제로는 COLON NEWLINE INDENT에서 INDENT로 변환했습니다.이 경우 모든 들여 쓰기 된 블록 앞에 항상 콜론이 오기 때문에) 파서는 그 위에 세 번째 단계였습니다. 하지만 특히 당신이 코드 생성 도구를 사용하고 있다면 아마도 렉서와 레이 아웃을 분리하기를 원한다면, 당신이 묘사 한 방식대로 일을하는 것이 나에게 많은 의미를가집니다. 예를 들어 일반적인 연습처럼 렉서를 만들 수 있습니다.

this line introduces an indented block of literal text: 
    this line of the block is indented four spaces 
    but this line is only indented two spaces 
다음은 특정 상황에서 유효하기 위해 필요한 다음, 예를 들면 -

나는 기본적으로 필요할 때를 시행하는 파서를 떠나, 들여 쓰기 규칙에 대한 좀 더 유연하게하기 위해 필요한 하나의 응용 프로그램을 가지고 있었다

인 덴트/데드먼트 토큰과 함께 잘 작동하지 않는데, 인 덴트의 각 열에 대해 하나의 인 덴트를 생성 할 필요가 생기고, 길에서 다시 같은 수의 DEDENT를 생성해야하므로, 들여 쓰기 수준이 끝나기 시작합니다. 토크 메이커가 원하지 않는 것처럼 보입니다. 이 경우 몇 가지 시도를했는데 다음 논리 행에 대해 들여 쓰기 (양수 또는 음수)가 변경된 각 NEWLINE 토큰에 카운터를 저장하는 것으로 끝났습니다. (각 토큰에는 보존이 필요한 경우를 대비해 후행 공백이 모두 저장되어 있었고 NEWLINE의 경우 저장된 공백에는 EOL 자체, 중간 빈 줄 및 다음 논리 줄의 들여 쓰기가 포함되었습니다.) 별도의 들여 쓰기 또는 이탤릭 토큰이 전혀 없습니다. 파서가 그것을 처리하도록하는 것은 단지 중첩하는 들여 쓰기와 DEDENTs보다 조금 더 많은 작업이었고, 멋진 파서 생성기를 필요로하는 복잡한 문법으로는 지옥 이었을지도 모르지만, 두려운만큼 나쁘지는 않았습니다. 어느 한 쪽. 다시 말하면 구문 분석기가 NEWLINE에서 앞을 내다 볼 필요가 없으며이 스키마에 색인이 있는지 확인할 수 있습니다.

여전히 tokenizer/layouter에서 모든 방식으로 미친듯한 공백을 허용하고 보존하고 파서가 리터럴과 코드가 무엇인지 결정하게하는 것은 약간 특이한 요구 사항입니다. 예를 들어 파이썬 코드를 파싱 할 수 있기를 원한다면 파서가 들여 쓰기 카운터에 안장되는 것을 원하지 않을 것입니다. 여러분이 일을하는 방식은 거의 확실히 여러분의 어플리케이션과 다른 많은 것들을위한 올바른 접근 방식입니다. 다른 사람들이 이런 종류의 일을하는 것이 최선의 방법에 대한 생각을 가지고 있지만, 나는 분명히 그들을 듣기를 좋아할 것입니다 ....

3

필자는 최근 이것을 실험 해 왔으며, 최소한, NEWLINES가 들여 쓰기 된 블록의 마지막 문장이든 아니든, 각 "statement"의 끝을 표시하기를 원했습니다. 즉, DEDENT 이전에 줄 바꿈이 필요합니다.

해결 방법 내 머리말을 바꾸는 것이 해결책이며 줄 바꿈을 나타내는 NEWLINES 대신 LINE 토큰을 사용하여 줄 시작을 표시합니다.

필자는 빈 줄 (주석 전용 줄 포함)을 축소하고 마지막 줄의 들여 쓰기에 대한 정보가 포함 된 단일 LINE 토큰을 방출하는 렉서를 사용합니다. 그런 다음 내 사전 처리 함수는이 토큰 스트림을 사용하고 들여 쓰기가 변경되는 모든 행 사이에 "들여 쓰기"또는 "내어 쓰기"를 추가합니다. 그래서

line1 
    line2 
    line3 
line4 

이 날 그들이 중첩, 들여 쓰기, 서브 블록, 뭔가로 끝나는 경우에도 문장의 끝을 감지에 대한 걱정없이 문에 대한 명확한 문법 제작을 작성할 수 있습니다

LINE "line1" INDENT LINE "line2" LINE "line3" DEDENT LINE "line4" EOF 

토큰 스트림을 줄 것 대신 NEWLINES (및 DEDENTS)와 일치하는 경우 어려울 수 있습니다. 여기

는 O'Caml 작성 전처리의 핵심입니다 : 내가 괄호 아직에 대한 지원을 추가하지 않은

match next_token() with 
     LINE indentation -> 
     if indentation > !current_indentation then 
      (
      Stack.push !current_indentation indentation_stack; 
      current_indentation := indentation; 
      INDENT 
     ) 
     else if indentation < !current_indentation then 
      (
      let prev = Stack.pop indentation_stack in 
       if indentation > prev then 
       (
        current_indentation := indentation; 
        BAD_DEDENT 
       ) 
       else 
       (
        current_indentation := prev; 
        DEDENT 
       ) 
     ) 
     else (* indentation = !current_indentation *) 
      let token = remove_next_token() in 
      if next_token() = EOF then 
       remove_next_token() 
      else 
       token 
    | _ -> 
     remove_next_token() 

, 그러나 그것은 단순한 확장해야한다. 그러나 파일 끝 부분에 누락 된 선을 방출하지 마십시오. 재미를 위해

+0

코드가 여러 DEDENT를 방출 할 수 없으며 EOF 이전에 헌정을 고려하지도 않습니다. 유용 할 수 있지만, 괄호 지원보다 중요합니다. – Cheery

+0

또한, 괄호에 대한 특별한 지원에 대해 신경 쓰지 마세요. 파이썬처럼 최선의 포인트를 놓치지 않을 것입니다. 레이아웃의 요점은 우수한 다중 행 구문을 제공 할 수 있도록 허용하는 것이며, 두 가지를 결합 할 수 없다면 괄호와 충돌하지 않습니다. – Cheery

+0

내 코드가 여러 DEDENT를 방출하므로 사용자가 잘못 읽고 있다고 생각합니다. 그러나 나는 파이썬보다는 하스켈처럼 보이는 것을 선호한다. 그래서 새로운 접근이 필요하다. – dkagedal

1

토큰 화 루비 :

def tokenize(input) 
    result, prev_indent, curr_indent, line = [""], 0, 0, "" 
    line_started = false 

    input.each_char do |char| 

    case char 
    when ' ' 
     if line_started 
     # Content already started, add it. 
     line << char 
     else 
     # No content yet, just count. 
     curr_indent += 1 
     end 
    when "\n" 
     result.last << line + "\n" 
     curr_indent, line = 0, "" 
     line_started = false 
    else 
     # Check if we are at the first non-space character. 
     unless line_started 
     # Insert indent and dedent tokens if indentation changed. 
     if prev_indent > curr_indent 
      # 2 spaces dedentation 
      ((prev_indent - curr_indent)/2).times do 
      result << :DEDENT 
      end 
      result << "" 
     elsif prev_indent < curr_indent 
      result << :INDENT 
      result << "" 
     end 

     prev_indent = curr_indent 
     end 

     # Mark line as started and add char to line. 
     line_started = true; line << char 
    end 

    end 

    result 
end 

는 두 공간 들여 쓰기에 대해서만 작업을 수행합니다. 결과는 ["Hello there from level 0\n", :INDENT, "This\nis level\ntwo\n", :DEDENT, "This is level0 again\n"]과 같습니다.

관련 문제