สิ่งที่ควรเป็นประเภทข้อมูลของโทเค็นที่ lexer กลับไปที่ parser ของมัน?


21

ตามที่ระบุไว้ในชื่อแล้วข้อมูลชนิดใดที่ lexer ส่งคืน / ให้ parser? เมื่ออ่านบทความวิเคราะห์คำศัพท์ที่ Wikipedia มีระบุว่า:

ในวิทยาการคอมพิวเตอร์การวิเคราะห์คำศัพท์เป็นกระบวนการของการแปลงลำดับของตัวละคร (เช่นในโปรแกรมคอมพิวเตอร์หรือหน้าเว็บ) เป็นลำดับโทเค็น ( สตริงที่มีการระบุ "ความหมาย")

อย่างไรก็ตามในความขัดแย้งที่สมบูรณ์ให้กับคำสั่งดังกล่าวเมื่อคำถามอื่นผมถามในเว็บไซต์ที่แตกต่างกัน ( รหัสการตรวจทานถ้าคุณอยากรู้) ได้รับการตอบคนตอบระบุไว้ว่า:

lexer มักจะอ่านสตริงและแปลงเป็นกระแส ... ของ lexemes คำศัพท์จะต้องเป็นตัวเลขของสตรีมเท่านั้น

และเขาให้ภาพนี้:

nl_output => 256
output    => 257
<string>  => 258

ต่อมาในบทความเขากล่าวถึงFlexเล็กซ์ที่มีอยู่แล้วและกล่าวว่าการเขียน 'กฎ' กับมันจะง่ายกว่าการเขียนเล็กซ์เซอร์ด้วยมือ เขาดำเนินการให้ฉันตัวอย่างนี้:

Space              [ \r\n\t]
QuotedString       "[^"]*"
%%
nl_output          {return 256;}
output             {return 257;}
{QuotedString}     {return 258;}
{Space}            {/* Ignore */}
.                  {error("Unmatched character");}
%%

เพื่อส่งเสริมความเข้าใจของฉันและได้รับข้อมูลมากขึ้นผมอ่านบทความเกี่ยวกับวิกิพีเดียFlex บทความ Flex แสดงให้เห็นว่าคุณสามารถกำหนดชุดของกฎไวยากรณ์ด้วยโทเค็นด้วยวิธีต่อไปนี้:

digit         [0-9]
letter        [a-zA-Z]

%%
"+"                  { return PLUS;       }
"-"                  { return MINUS;      }
"*"                  { return TIMES;      }
"/"                  { return SLASH;      }
"("                  { return LPAREN;     }
")"                  { return RPAREN;     }
";"                  { return SEMICOLON;  }
","                  { return COMMA;      }
"."                  { return PERIOD;     }
":="                 { return BECOMES;    }
"="                  { return EQL;        }
"<>"                 { return NEQ;        }
"<"                  { return LSS;        }
">"                  { return GTR;        }
"<="                 { return LEQ;        }
">="                 { return GEQ;        }
"begin"              { return BEGINSYM;   }
"call"               { return CALLSYM;    }
"const"              { return CONSTSYM;   }
"do"                 { return DOSYM;      }
"end"                { return ENDSYM;     }
"if"                 { return IFSYM;      }
"odd"                { return ODDSYM;     }
"procedure"          { return PROCSYM;    }
"then"               { return THENSYM;    }
"var"                { return VARSYM;     }
"while"              { return WHILESYM;   }

สำหรับฉันแล้ว Flex lexer ส่งคืนสตริงคำหลัก \ โทเค็น แต่มันอาจเป็นการคืนค่าคงที่ที่เท่ากับจำนวนที่แน่นอน

ถ้า lexer กำลังส่งคืนตัวเลขมันจะอ่านตัวอักษรสตริงได้อย่างไร การส่งคืนตัวเลขเป็นสิ่งที่ดีสำหรับคำหลักเดียว แต่คุณจะจัดการกับสตริงได้อย่างไร lexer ไม่ต้องแปลงสตริงเป็นเลขฐานสองจากนั้นตัวแยกวิเคราะห์จะแปลงตัวเลขกลับเป็นสตริง ดูเหมือนว่าตรรกะ (และง่ายกว่า) สำหรับ lexer ในการส่งคืนสตริงและให้ parser แปลงตัวอักษรสตริงจำนวนใด ๆ ให้เป็นตัวเลขจริง

หรือผู้ที่เป็นไปได้ของ lexer สามารถกลับมาทั้งสองได้หรือไม่ ฉันได้พยายามที่จะเขียน lexer ง่ายใน c ++ ซึ่งจะช่วยให้คุณมีเพียงหนึ่งชนิดกลับสำหรับการทำงานของคุณ ดังนั้นฉันจึงถามคำถาม

เพื่อย่อคำถามของฉันลงในย่อหน้า: เมื่อเขียน lexer และสมมติว่ามันสามารถส่งคืนชนิดข้อมูล (สตริงหรือตัวเลข) หนึ่งชนิดเท่านั้นซึ่งจะเป็นตัวเลือกที่มีเหตุผลมากกว่านี้


lexer ส่งคืนสิ่งที่คุณบอกให้ส่งคืน หากการออกแบบของคุณเรียกหาหมายเลขก็จะส่งคืนหมายเลข เห็นได้ชัดว่าการเป็นตัวแทนของตัวอักษรสตริงจะต้องมีมากกว่านั้น ดูเพิ่มเติมหน้าที่ของ Lexer คือการแยกหมายเลขและสายอักขระ? โปรดสังเกตว่าโดยทั่วไปตัวอักษรสตริงจะไม่ถือว่าเป็น "องค์ประกอบภาษา"
Robert Harvey

@RobertHarvey คุณจะแปลงสตริงตัวอักษรให้เป็นเลขฐานสองหรือไม่
Christian Dean

ตามที่ฉันเข้าใจวัตถุประสงค์ของ lexer คือการใช้องค์ประกอบภาษา (เช่นคำหลักตัวดำเนินการเป็นต้น) และเปลี่ยนเป็นโทเค็น ดังนั้นสตริงที่ยกมาจึงไม่น่าสนใจสำหรับ lexer เพราะมันไม่ใช่องค์ประกอบทางภาษา แม้ว่าฉันจะไม่เคยเขียน lexer ด้วยตัวเอง แต่ฉันก็จินตนาการว่าสตริงที่ยกมานั้นเพิ่งผ่านไปโดยไม่เปลี่ยนแปลง (รวมถึงคำพูด)
Robert Harvey

ดังนั้นสิ่งที่คุณพูดคือ lexer ไม่อ่านหรือสนใจเกี่ยวกับตัวอักษรสตริง parser จะต้องมองหาตัวอักษรสตริงเหล่านี้เหรอ? มันสับสนมาก
Christian Dean

คุณอาจต้องการใช้เวลาสองสามนาทีในการอ่านสิ่งนี้: en.wikipedia.org/wiki/Lexical_analysis
Robert Harvey

คำตอบ:


10

โดยทั่วไปหากคุณกำลังประมวลผลภาษาแม้ว่า lexing และการแยกวิเคราะห์คุณมีคำจำกัดความของโทเค็นศัพท์ของคุณเช่น:

NUMBER ::= [0-9]+
ID     ::= [a-Z]+, except for keywords
IF     ::= 'if'
LPAREN ::= '('
RPAREN ::= ')'
COMMA  ::= ','
LBRACE ::= '{'
RBRACE ::= '}'
SEMICOLON ::= ';'
...

และคุณมีไวยากรณ์สำหรับโปรแกรมแยกวิเคราะห์:

STATEMENT ::= IF LPAREN EXPR RPAREN STATEMENT
            | LBRACE STATEMENT BRACE
            | EXPR SEMICOLON
EXPR      ::= ID
            | NUMBER
            | ID LPAREN EXPRS RPAREN
...

lexer ของคุณใช้สตรีมอินพุตและสร้างโทเค็นสตรีม สตรีมของโทเค็นถูกใช้งานโดยตัวแยกวิเคราะห์เพื่อสร้างต้นแจง ในบางกรณีเพียงแค่รู้ว่าชนิดของโทเค็นนั้นเพียงพอ (เช่น LPAREN, RBRACE, FOR) แต่ในบางกรณีคุณจะต้องใช้ค่าจริงที่เชื่อมโยงกับโทเค็น ตัวอย่างเช่นเมื่อคุณพบโทเค็น ID คุณจะต้องการตัวอักษรจริงที่ประกอบขึ้นเป็น ID ในภายหลังเมื่อคุณกำลังพยายามหาตัวระบุที่คุณกำลังพยายามอ้างอิง

ดังนั้นโดยทั่วไปคุณจะมีอะไรมากกว่านี้หรือน้อยกว่า:

enum TokenType {
  NUMBER, ID, IF, LPAREN, RPAREN, ...;
}

class Token {
  TokenType type;
  String value;
}

ดังนั้นเมื่อ lexer ส่งคืนโทเค็นคุณจะรู้ว่ามันคืออะไร (ซึ่งคุณต้องการสำหรับการแยกวิเคราะห์) และลำดับของอักขระที่สร้างจาก (ซึ่งคุณจะต้องใช้ในภายหลังเพื่อตีความสตริงและตัวอักษรตัวเลขตัวบ่งชี้ ฯลฯ ) อาจรู้สึกว่าคุณคืนค่าสองค่าเนื่องจากคุณคืนประเภทรวมที่ง่ายมาก แต่คุณต้องการทั้งสองส่วน ท้ายที่สุดคุณต้องปฏิบัติต่อโปรแกรมต่าง ๆ ดังต่อไปนี้:

if (2 > 0) {
  print("2 > 0");
}
if (0 > 2) {
  print("0 > 2");
}

สิ่งเหล่านี้สร้างลำดับโทเค็นประเภทเดียวกัน : IF, LPAREN, NUMBER, GREATER_THAN, NUMBER, RPAREN, LBRACE, ID, LPAREN, STRING, RPAREN, SEMICOLON, RBRACE นั่นหมายความว่าพวกเขาแยกกันเหมือนกัน แต่เมื่อคุณทำอะไรบางอย่างกับทรีแยกวิเคราะห์คุณจะสนใจว่าค่าของหมายเลขแรกคือ '2' (หรือ '0') และค่าของตัวเลขที่สองคือ '0' (หรือ '2 ') และค่าของสตริงคือ' 2> 0 '(หรือ' 0> 2 ')


ฉันจะได้รับสิ่งที่คุณพูด แต่วิธีที่String valueจะได้รับการเติม? มันจะเต็มไปด้วยสตริงหรือตัวเลขหรือไม่? และฉันจะกำหนดStringประเภทได้อย่างไร
Christian Dean

1
@ Mr.Python ในกรณีที่ง่ายที่สุดมันเป็นเพียงสตริงของตัวละครที่ตรงกับการผลิตคำศัพท์ ดังนั้นถ้าคุณเห็นfoo (23, "bar")คุณจะได้รับโทเค็น[ID, "foo"], [LPAREN, "("], [NUMBER, "23"], [COMMA, "," ], [STRING "" 23 ""], [RPAREN, ")"] การรักษาข้อมูลนั้นอาจมีความสำคัญ หรือคุณสามารถใช้วิธีอื่นและมีค่าที่มีประเภทยูเนี่ยนที่สามารถเป็นสตริงหรือตัวเลข ฯลฯ และเลือกประเภทค่าที่เหมาะสมขึ้นอยู่กับชนิดของโทเค็นประเภทที่คุณมี (เช่นเมื่อประเภทโทเค็นเป็นจำนวน ใช้ value.num และเมื่อเป็น STRING ให้ใช้ value.str)
Joshua Taylor

@MrPython "และฉันจะกำหนดประเภทของสตริงได้อย่างไร" ฉันเขียนจากแนวคิด Java-ish หากคุณทำงานใน C ++ คุณสามารถใช้ชนิดสตริงของ C ++ หรือหากคุณทำงานใน C คุณสามารถใช้อักขระ char * จุดคือที่เกี่ยวข้องกับโทเค็นคุณมีค่าที่สอดคล้องกันหรือข้อความที่คุณสามารถตีความเพื่อสร้างมูลค่า
Joshua Taylor

1
@ ollydbg23 เป็นตัวเลือกไม่ใช่ตัวเลือกที่ไม่สมเหตุสมผล แต่มันทำให้ระบบมีความสอดคล้องภายในน้อยลง ตัวอย่างเช่นหากคุณต้องการให้ค่าสตริงของเมืองสุดท้ายที่คุณแยกวิเคราะห์ตอนนี้คุณต้องตรวจสอบค่า Null อย่างชัดเจนแล้วใช้การค้นหาโทเค็นต่อสตริงแบบย้อนกลับเพื่อค้นหาว่าสตริงควรเป็นอย่างไร นอกจากนี้ยังมีการมีเพศสัมพันธ์ที่เข้มงวดมากขึ้นระหว่าง lexer และ parser มีรหัสเพิ่มเติมให้อัปเดตหาก LPAREN สามารถจับคู่สตริงที่แตกต่างกันหรือหลายสายได้
Joshua Taylor

2
@ ollydbg23 กรณีหนึ่งจะเป็นหลอกหลอกง่าย ๆ ง่ายพอที่จะทำparse(inputStream).forEach(token -> print(token.string); print(' '))(เช่นเพียงพิมพ์ค่าสตริงของโทเค็นคั่นด้วยช่องว่าง) มันค่อนข้างเร็ว และแม้ว่า LPAREN สามารถมาจาก "(" เท่านั้นที่อาจเป็นสตริงคงที่ในหน่วยความจำดังนั้นการรวมการอ้างอิงถึงโทเค็นอาจไม่แพงกว่าการรวมการอ้างอิงโมฆะโดยทั่วไปฉันควรเขียน รหัสที่ไม่ได้ทำให้กรณีพิเศษฉันรหัสใด ๆ .
โจชัวเทย์เลอร์

6

ตามที่ระบุไว้ในชื่อแล้วข้อมูลชนิดใดที่ lexer ส่งคืน / ให้ parser

"โทเค็น" แน่นอน lexer สร้างกระแสโทเค็นดังนั้นจึงควรส่งกระแสโทเค็นกลับมา

เขากล่าวถึง Flex, lexer ที่มีอยู่แล้วและกล่าวว่าการเขียน 'กฎ' ด้วยมันจะง่ายกว่าการเขียน lexer ด้วยมือ

lexers ที่สร้างด้วยเครื่องจักรมีข้อได้เปรียบที่คุณสามารถสร้างได้อย่างรวดเร็วซึ่งมีประโยชน์อย่างยิ่งหากคุณคิดว่าไวยากรณ์ศัพท์ของคุณกำลังเปลี่ยนแปลงไปมาก พวกเขามีข้อเสียที่คุณมักจะไม่ได้รับความยืดหยุ่นมากในตัวเลือกการใช้งานของคุณ

ที่กล่าวว่าใครสนใจว่า "ง่ายกว่า" ใคร การเขียน lexer มักไม่ใช่ส่วนที่ยาก!

เมื่อเขียน lexer และสมมติว่ามันสามารถคืนค่าได้เพียงหนึ่งชนิดข้อมูล (สตริงหรือตัวเลข) ซึ่งจะเป็นตัวเลือกที่มีเหตุผลมากกว่านี้?

ทั้ง lexer มักจะมี "ต่อไป" การดำเนินการที่ผลตอบแทนโทเค็นดังนั้นจึงควรกลับโทเค็น โทเค็นไม่ใช่สตริงหรือตัวเลข มันเป็นสัญลักษณ์

lexer ตัวสุดท้ายที่ฉันเขียนคือ lexer "full fidelity" ซึ่งหมายความว่ามันจะส่งคืนโทเค็นที่ติดตามตำแหน่งของช่องว่างและความคิดเห็นทั้งหมด - ซึ่งเราเรียกว่า "trivia" - ในโปรแกรมเช่นเดียวกับโทเค็น ใน lexer ของฉันโทเค็นถูกกำหนดเป็น:

  • อาร์เรย์ของเรื่องไม่สำคัญชั้นนำ
  • ประเภทโทเค็น
  • ความกว้างโทเค็นเป็นอักขระ
  • อาร์เรย์ของเรื่องไม่สำคัญต่อท้าย

เรื่องไม่สำคัญถูกกำหนดเป็น:

  • ประเภทเรื่องไม่สำคัญ - ช่องว่างบรรทัดใหม่ความคิดเห็นและอื่น ๆ
  • ความกว้างเรื่องไม่สำคัญในตัวละคร

ดังนั้นหากเรามีสิ่งที่ชอบ

    foo + /* comment */
/* another comment */ bar;

ที่จะไฟแนนเชี่สี่ราชสกุลกับโทเค็นชนิดIdentifier, Plus, Identifier, Semicolon, และความกว้าง 3, 1, 3, 1. ตัวบ่งชี้แรกที่มีเรื่องไม่สำคัญประกอบด้วยชั้นนำWhitespaceที่มีความกว้าง 4 และต่อท้ายเรื่องไม่สำคัญWhitespaceกับความกว้างของ 1. Plusไม่มีเรื่องไม่สำคัญชั้นนำและ เรื่องไม่สำคัญต่อท้ายประกอบด้วยหนึ่งช่องว่างความคิดเห็นและขึ้นบรรทัดใหม่ ตัวระบุสุดท้ายมีเรื่องไม่สำคัญชั้นนำของความคิดเห็นและช่องว่างและอื่น ๆ

ด้วยโครงร่างนี้อักขระทุกตัวในไฟล์จะถูกนำมาคิดในผลลัพธ์ของ lexer ซึ่งเป็นคุณสมบัติที่มีประโยชน์สำหรับสิ่งต่าง ๆ เช่นการระบายสีไวยากรณ์

แน่นอนถ้าคุณไม่ต้องการเรื่องไม่สำคัญคุณสามารถสร้างโทเค็นได้สองอย่าง: ชนิดและความกว้าง

คุณอาจสังเกตเห็นว่าโทเค็นและเรื่องไม่สำคัญมีเพียงความกว้างไม่ใช่ตำแหน่งที่แน่นอนในซอร์สโค้ด นั่นคือเจตนา โครงการดังกล่าวมีข้อดี:

  • มันมีขนาดกะทัดรัดในหน่วยความจำและรูปแบบสายไฟ
  • มันเปิดใช้งานการแก้ไขอีกครั้ง สิ่งนี้มีประโยชน์หาก lexer กำลังทำงานอยู่ภายใน IDE นั่นคือถ้าคุณตรวจพบการแก้ไขในโทเค็นคุณเพียงแค่สำรองไลเซกเตอร์ของคุณเป็นโทเค็นคู่ก่อนการแก้ไขและเริ่ม lexing อีกครั้งจนกว่าคุณจะซิงค์กับโทเค็นสตรีมก่อนหน้า เมื่อคุณพิมพ์อักขระตำแหน่งของโทเค็นทุกตัวหลังจากเปลี่ยนอักขระ แต่โดยปกติจะมีเพียงหนึ่งหรือสองโทเค็นที่เปลี่ยนความกว้างดังนั้นคุณจึงสามารถใช้สถานะนั้นได้ทั้งหมด
  • ออฟเซ็ตอักขระที่แน่นอนของโทเค็นทุกตัวสามารถรับได้ง่ายโดยวนซ้ำโทเค็นสตรีมและติดตามออฟเซ็ตปัจจุบัน เมื่อคุณมีการหักล้างตัวอักษรที่แน่นอนแล้วมันเป็นเรื่องง่ายที่จะแยกข้อความเมื่อจำเป็น

หากคุณไม่สนใจสถานการณ์ใด ๆ เหล่านั้นโทเค็นอาจถูกแทนด้วยชนิดและออฟเซ็ตแทนที่จะเป็นชนิดและความกว้าง

แต่ Takeaway ที่สำคัญที่นี่คือการเขียนโปรแกรมเป็นศิลปะของการทำนามธรรมที่มีประโยชน์ คุณกำลังจัดการโทเค็นดังนั้นให้ทำสิ่งที่เป็นประโยชน์เกี่ยวกับโทเค็นให้เป็นประโยชน์


3

โดยทั่วไปแล้วคุณจะส่งคืนโครงสร้างขนาดเล็กที่มีหมายเลขซึ่งบ่งบอกถึงโทเค็น (หรือค่า enum เพื่อความสะดวกในการใช้งาน) และค่าทางเลือก (สตริงหรือค่าทั่วไป / เทมเพลต) อีกวิธีคือการส่งคืนชนิดที่ได้รับสำหรับองค์ประกอบที่ต้องมีข้อมูลเพิ่มเติม ทั้งสองอย่างน่ารังเกียจอย่างอ่อนโยน แต่ก็ดีพอที่จะแก้ปัญหาในทางปฏิบัติ


คุณหมายถึงอะไรที่น่ารังเกียจอย่างอ่อนโยน ? พวกเขาไม่มีประสิทธิภาพในการรับค่าสตริงหรือไม่
Christian Dean

@ Mr.Python - พวกเขาจะนำไปสู่การตรวจสอบจำนวนมากก่อนที่จะใช้ในรหัสซึ่งไม่มีประสิทธิภาพ แต่ moreso ทำให้รหัสซับซ้อน / เปราะบางเล็กน้อย
Telastyn

ฉันมีคำถามที่คล้ายกันเมื่อออกแบบ lexer ใน C ++ ฉันสามารถส่งคืน a Token *หรือเพียงแค่ a Tokenหรือ a TokenPtrซึ่งเป็นตัวชี้ที่ใช้ร่วมกันของTokenชั้นเรียน แต่ฉันก็เห็น lexer คืนค่าเพียง TokenType และเก็บค่าสตริงหรือตัวเลขในตัวแปรโกลบอลหรือสแตติกอื่น ๆ คำถามอื่นคือเราจะเก็บข้อมูลที่ตั้งได้อย่างไรฉันต้องมีโครงสร้าง Token ซึ่งมีเขตข้อมูล TokenType, String และตำแหน่งที่ตั้งหรือไม่ ขอบคุณ
ollydbg23

@ ollydbg23 - สิ่งเหล่านี้สามารถทำงานได้ ฉันจะใช้ struct และสำหรับภาษาที่ไม่ได้เรียนรู้คุณจะใช้เครื่องมือสร้างวิเคราะห์คำ
Telastyn

@Telastyn ขอบคุณสำหรับการตอบกลับ คุณหมายถึงโครงสร้างของโทเค็นอาจเป็นอะไรstruct Token {TokenType id; std::string lexeme; int line; int column;}ก็ได้ สำหรับฟังก์ชั่นของประชาชน Lexer เช่นPeekToken()ฟังก์ชั่นจะกลับมาหรือToken * TokenPtrฉันคิดว่าสักพักถ้าฟังก์ชั่นเพิ่งคืน TokenType Parser จะพยายามรับข้อมูลอื่นเกี่ยวกับ Token อย่างไร ดังนั้นตัวชี้เช่นประเภทข้อมูลจึงเป็นที่ต้องการสำหรับการส่งคืนจากฟังก์ชันดังกล่าว ความคิดเห็นใด ๆ เกี่ยวกับความคิดของฉัน? ขอบคุณ
ollydbg23
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.