[go: up one dir, main page]

0% found this document useful (0 votes)
156 views48 pages

Chapter 2-Lexical Analysis

Uploaded by

somsonengda
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PPTX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
156 views48 pages

Chapter 2-Lexical Analysis

Uploaded by

somsonengda
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PPTX, PDF, TXT or read online on Scribd
You are on page 1/ 48

Principles of Compiler Design (SENG 3042 )

Chapter 2
Lexical Analysis

1
Objective
At the end of this session students will be able to:
 Understand the basic roles of lexical analyzer (LA): Lexical Analysis Versus Parsing,

Tokens , Patterns, and Lexemes, Attributes for Tokens and Lexical Errors.
 Understand the specification of Tokens: Strings and Languages, Operations on

Languages, Regular Expressions, Regular Definitions and Extensions of Regular


Expressions

 Understand the generation of Tokens: Transition Diagrams, Recognition of Reserved

Words and Identifiers, Completion of the Running Example, Architecture of a

Transition-Diagram-Based Lexical Analysis.

 Understand the basics of Automata: Nondeterministic Finite Automata (NFA) Versus

Deterministic Finite Automata(DFA), and conversion of an NFA to a DFA.

 Understand the Javacc, A Lexical Analyzer and Parser Generator: Structure of


2
Introduction
 The lexical analysis phase of compilation breaks the text file (program) into smaller

chunks called tokens.

 A token describes a pattern of characters having same meaning in the source

program. (such as identifiers, operators, keywords, numbers, delimiters and so on)


The lexical analysis phase of the compiler is often called tokenization
 The lexical analyzer (LA) reads the stream of characters making up the source

program and groups the characters into meaningful sequences called lexemes.

 For each lexeme, the lexical analyzer produces as output a token of the form {token-

name, attribute-value} that it passes on to the subsequent phase, syntax analysis.

Where,
 token- name is an abstract symbol that is used during syntax analysis , and
 attribute-value points to an entry in the symbol table for this token.
3
Example
 For example, suppose a source program contains the assignment statement

posit ion = initial + rate * 60


 The characters in this assignment could be grouped into the following lexemes and

mapped into the following tokens passed on to the syntax analyzer:

1. position is a lexeme that would be mapped into a token {id, 1}, where id is an
abstract symbol standing for identifier and 1 points to the symbol table entry for
position.
 The symbol-table entry for an identifier holds information about the identifier,
such as its name and type.

2. The assignment symbol = is a lexeme that is mapped into the token {=}. Since this

token needs no attribute-value, the second component is omitted.


 We could have used any abstract symbol such as assign for the token-name , but for
notational convenience we have chosen to use the lexeme itself as the name of the
abstract symbol.

3. initial is a lexeme that is mapped into the token {id, 2} , where 2 points to the 4
Contd…
4. + is a lexeme that is mapped into the token {+}.

5. rate is a lexeme that is mapped into the token { id, 3 }, where 3 points to the

symbol-table entry for rate.

6. * is a lexeme that is mapped into the token {* }.

7. 60 is a lexeme that is mapped into the token { 60 }.

Blanks separating the lexemes would be discarded by the lexical analyzer.

5
Contd…
Parser uses tokens
produced by the LA to
create a tree-like
intermediate representation
that depicts the
grammatical structure of
 Ittheuses
token
thestream.
syntax tree and
the information in the
symbol table to check the
source program for
semantic consistency with
the language definition.
 An important part of
semantic analysis is type
In the process of translating a checking, where the
source program into target code, a compiler checks that each
compiler may construct one or operator has matching
more intermediate representations, operands
which are easy to produce and The machine-independent code-
easy to translate into the target optimization phase attempts to
machine improve the intermediate code so
that better target code will result.
Usually better means faster, but
other objectives may be desired,
such as shorter code, or target
code that consumes less power.
6
The Role of Lexical Analyzer
Source token To semantic
Lexical
program Parser analysis
Analyzer
getNextToken

Symbol
Table

 It is common for the lexical analyzer to interact with the symbol table.

When the lexical analyzer discovers a lexeme constituting an identifier, it

needs to enter that lexeme into the symbol table.

In some cases, information regarding the kind of identifier may be read from

the symbol table by the lexical analyzer to assist it in determining the proper

token it must pass to the parser. 7


Other tasks of Lexical Analyzer
 Since LA is the part of the compiler that reads the source text, it may perform

certain other tasks besides identification of lexemes such as:


1. Stripping out comments and whitespace (blank, newline, tab, and perhaps
other characters that are used to separate tokens in the input).
2. Correlating error messages generated by the compiler with the source program.
 For instance, the LA may keep track of the number of newline characters
seen, so it can associate a line number with each error message.
3. If the source program uses a macro-preprocessor, the expansion of macros may
also be performed by the lexical analyzer.
 Sometimes, lexical analyzers are divided into a cascade of two processes:

A. Scanning consists of the simple processes that do not require tokenization of


the input, such as deletion of comments and compaction of consecutive
whitespace characters into one.
B. Lexical analysis proper is the more complex portion, where the scanner
produces the sequence of tokens as output 8
Lexical Analysis Vs Parsing
 There are a number of reasons why the analysis portion of a compiler is normally

separated into lexical analysis and parsing (syntax analysis) phases:


1. Simplicity of Design:- is the most important consideration that often allows us to

simplify compilation tasks.


For example, a parser that had to deal with comments and whitespace as syntactic
units would be considerably more complex than one that can assume comments
and whitespace have already been removed by the lexical analyzer.
If we are designing a new language, separating lexical and syntactic concerns can
lead to a cleaner overall language design.
2. Compiler efficiency is improved:- A separate lexical analyzer allows us to apply

specialized techniques that serve only the lexical task, not the job of parsing.
In addition, specialized buffering techniques for reading input characters can
speed up the compiler significantly.
3. Compiler portability is enhanced:- Input-device-specific peculiarities can be

restricted to the lexical analyzer. 9


Tokens, Patterns, and Lexemes
A lexeme is a sequence of characters in the source program that matches the pattern for a
token.
Example: -5, i, sum, 100, for ; int 10 + -
 One
Token:- is a pair consisting of a token tokenand
name foran
each keyword.
optional The pattern
attribute . a keyword is the
valuefor
same as the keyword itself.
Example:
 Tokens for the operators , either individually or in classes
Identifiers = { i, sum } such as the token in comparison operators
 One token representing all identifiers.
int_Constatnt = { 10, 100, -5 }  One or more tokens representing constants, such as numbers
Oppr = { +, -} and literal strings .
 Tokens for each punctuation symbol, such as left and right
rev_Words = { for, int} parentheses, comma, and semicolon
Separators = { ;, ,}

Pattern is a rule describing the set of lexemes that can represent a particular token in

source program
It is a description of the form that the lexemes of a token may take.
In the case of a keyword as a token, the pattern is just the sequence of characters 10
Contd…
 One efficient but complex brute-force approach is to read character by character

to check if each one follows the right sequence of a token.


 The below example shows a partial code for this brute-force lexical analyzer.

 What we’d like is a set of tools that will allow us to easily create and modify a

lexical analyzer that has the same run-time efficiency as the brute-force
method.
 =The
if (c first tool==
nextchar() that‘c’)
we{ use to attack this problem is Deterministic Finite Automata
if(DFA).
(c = nextchar() == ‘l’) {
// code to handle the rest of either “class” or any identifier that starts
with “cl”
} else if (c == ‘a’) {
// code to handle the rest of either “case” or any identifier that starts
with “ca”
} else {
// code to handle any identifier that starts with c
}
} else if ( c = …..) { 11
Lexical analysis and Types of token
Types of token
Contd…

14
Lexical Analyzer - Toenization

Count the number of tokens in a given code segment


NFA
DFA
Consideration for a Simple Design of LA
Lexical Analyzer can allow source program to be
1. Free-Format Input:- the alignment of lexeme should not be necessary in determining
the correctness of the source program such restriction put extra load on Lexical Analyzer
2. Blanks Significance:- Simplify the task of identifying tokens
E.g. Int a indicates <Int is keyword> <a is identifier>
Inta indicates <Inta is Identifier>
3. Keyword must be reserved:- Keywords should be reserved otherwise LA will have to
predict whether the given lexeme should be treated as a keyword or as identifier
E.g. if then then then =else;
Else else = then;
The above statements are misleading as then and else keywords are not reserved.
Approaches to implementation
 Use assembly language- Most efficient but most difficult to implement

 Use high level languages like C- Efficient but difficult to implement

 Use tools like Lex, flex, javacc… Easy to implement but not as efficient as the first
19
Lexical Errors
 Are primarily of two kinds.

1. Lexemes whose length exceed the bound specified by the language.

 Most languages have bound on the precision of numeric constants.

 A constant whose bound exceeds this bound is a lexical error.

2. Illegal character in the program

 Characters like ~, , ,  occurring in a given programming

language (but not within a string or comment) are lexical errors.

3. Un terminated strings or comments.

20
Handling Lexical Errors
 It is hard for a LA to tell , without the aid of other components, that there is a

source-code error.
 For instance, if the string fi is encountered for the first time in a java program

in the context : fi ( a == 2*4+3 ) a lexical analyzer cannot tell whether fi is a

misspelling of the keyword if or an undeclared function identifier.


 However, suppose a situation arises in which the lexical analyzer is unable to

proceed because none of the patterns for tokens matches any prefix of the

remaining input.
 The simplest recovery strategy is "panic mode" recovery.

 We delete successive characters from the remaining input , until the lexical

analyzer can find a well-formed token at the beginning of what input is left.
 This recovery technique may confuse the parser, but in an interactive computing
21
Contd…
 Other possible error-recovery actions are:

1. Insert a missing character into the remaining input.

2. Replacing an incorrect character by a correct character

3. Transpose two adjacent characters(such as , fi=>if).

4. Deleting an extraneous character

5. Pre-scanning

22
Specification of Tokens
 Regular expressions are an important notation for specifying lexeme patterns.

 While they cannot express all possible patterns, they are very effective in specifying

those types of patterns that we actually need for tokens.

 In theory of compilation regular expressions are used to formalize the

specification of tokens

 Regular expressions are means for specifying regular languages

Strings and Languages

 An alphabet is any finite set of symbols. Typical examples of symbols are let­ters ,

digits, and punctuation.

 The set {0, 1 } is the binary alphabet.

 ASCII is an important example of an alphabet ; it is used in many software

systems. 27
Contd…
 A string over an alphabet is a finite sequence of symbols drawn from that

alphabet .
 In language theory, the terms "sentence" and "word" are often used as
synonyms for "string."
 The length of a string s, usually written |s|, is the number of occurrences of

symbols in s.
 For example, banana is a string of length six.

 The empty string, denoted Ɛ, is the string of length zero.

 A language is any countable set of strings over some fixed alphabet.

 Abstract languages like  , the empty set, or {Ɛ} , the set containing only the

empty string, are languages under this definition.


 So too are the set of all syntactically well-formed java programs and the set

of all grammatically correct English sentences, although the latter two languages
28
Terms for Parts of String
 The following string-related terms are commonly used:

1. A prefix of string S is any string obtained by removing zero or more symbols from the
end of s.
 For example, ban, banana, and Ɛ are prefixes of banana.
2. A suffix of string s is any string obtained by removing zero or more symbols from
the beginning of s.
 For example, nana, banana, and Ɛ are suffixes of banana.
3. A substring of s is obtained by deleting any prefix and any suffix from s.
 For instance, banana, nan, and Ɛ are substrings of banana.
4. The proper prefixes, suffixes, and substrings of a string s are those, prefixes,
suffixes, and substrings, respectively, of s that are not Ɛ or not equal to s itself.
5. A subsequence of s is any string formed by deleting zero or more not necessarily
consecutive positions of s.
29

Operations on Languages
The following string-related terms are commonly used:
 In lexical analysis, the most important operations on languages are union,

concatenation, and closure, which are defined formally below:


1. Union (U):- is the familiar operation on sets.
Example Union of L and M, L U M = {s|s is in L or s is in M}
2. Concatenation :- is all strings formed by taking a string from the first language and a
string from the second language, in all possible ways , and concatenating them.
Example Concatenation of L and M, LM = {st|s is in L and t is in M}
3. Kleene closure:- the kleene closure of a language L, denoted by L*, is the set of strings
you get by concatenating L zero, or more times.
Example Kleene closure of L,
Note that:- L0 , the "concatenation of L zero times," is defined to be {Ɛ} , and inductively, Li is
Li-1 L.
4. Positive closure:- is the positive closure of a language L, denoted by L+, is the same as
30
0 +
example
Contd…
Let L be the set of letters {A, B , . . . , Z , a, b, . . . , z} and let D be the set of digits {0, 1, . . . 9}.
We may think of L and D in two, essentially equivalent, ways.
 One way is that L and D are, respectively, the alphabets of uppercase and lowercase

letters and of digits.


 The second way is that L and D are languages, all of whose strings happen to be of

length one.
 Here are some other languages that can be constructed from languages L and D, using

the above operators:


1. L U D is the set of letters and digits - strictly speaking the language with 62
strings of length one, each of which strings is either one letter or one digit.
2. LD is the set of 520 strings of length two, each consisting of one letter followed by one
digit.

3. L4 is the set of all 4-letter strings.


32
4. L* is the set of ail strings of letters, including E, the empty string.
Regular Expressions
 In theory of compilation regular expressions are used to formalize the specification of

tokens
 Regular expressions are means for specifying regular languages
Example: ID  letter_(letter|digit)*
letter A|B|…|Z|a|b|…|z|_
digit 0|1|2|…|9
 Each regular expression is a pattern specifying the form of strings
 The regular expressions are built recursively out of smaller regular expressions,

using the following two rules:


R1:- Ɛ is a regular expression, and L(Ɛ) = {Ɛ}, that is , the language whose sole member
is the empty string.
R2:- If a is a symbol in ∑ then a is a regular expression, L(a) = {a}, that is , the language
with one string, of length one , with a in its one position.
Note:- By convention, we use italics for symbols, and boldface for their corresponding
regular expression.
 Each regular expression r denotes a language L(r), which is also defined 33
Example
Contd…
 There are four parts to the induction whereby larger regular expressions are

built from smaller ones.


 Suppose r and s are regular expressions denoting languages L(r) and L(s) ,
respectively.
1. (r) |( s) is a regular expression denoting the language L(r) U L(s) .
2. (r) (s) is a regular expression denoting the language L(r)L (s) .
3. (r )* is a regular expression denoting (L (r) )*.
4. (r) is a regular expression denoting L(r) .
 This last rule says that we can add additional pairs of parentheses around
expressions without changing the language they denote.
 Regular expressions often contain unnecessary pairs of parentheses .
 We may drop certain pairs of parentheses if we adopt the conventions that :

a. The unary operator * has highest precedence and is left associative.


b. Concatenation has second highest precedence and is left associative.
c. has lowest precedence and is left associative.
Example:- (a) |( (b) *(c)) can be represented by a|b*c. Both expressions denote the 35
Contd…
 A language that can be defined by a regular expression is called a regular set.
 If two regular expressions r and s denote the same regular set , we say they are

equivalent and write r = s.


 For instance, (a|b) = (b|a).
 There are a number of algebraic laws for regular expressions; each law asserts that

expressions of two different forms are equivalent.


LAW DESCRIPTION
r|s = s|r | is commutative
r|(s|t) = (r|s)|t |is associative
r(st) = (rs)t Concatenation is associative
r(s|t) = rs|rt; Concatenation distributes over |
(s|t)r= sr|tr
Ɛr=rƐ=r Ɛ is the identity for concatenation
r* = (r|Ɛ)* Ɛ is guaranteed in a closure
r** = r* * is idempotent

Table: The algebraic laws that hold for arbitrary regular expressions r, s, 36
Regular Definitions
 If ∑ is an alphabet of basic symbols, then a regular definition is a sequence of
Where
definitions of the form :
1. Each di is a new symbol, not in ∑ and not
d1  r1
the same as any other of the d's, and
d2  r2
2. Each ri is a regular expression over the

alphabet ∑ U { d1 ,d2 , ... ,di-1}.
dn  rn
Examples (b) Regular Definition for Java statements
(a) Regular Definition for Java Identifiers
stmt  if expr then stmt
ID  letter(letter|digit)* | if expr then stmt else stmt
letter A|B|…|Z|a|b|…|z|_ |Ɛ
expr  term relop term
(c) Regular 0|1|2|…|9
digitDefinition for unsigned numbers
| term
such as 5280 , 0.0 1 234, 6. 336E4, or 1.
89E-4 term  id
digit 0|1|2|…|9 | number
digits digit digit*
optFrac . digts|Ɛ
optExp (E(+|-|Ɛ) digts|Ɛ 37
Recognition of Regular Expressions
1. Starting point is the language grammar to understand the tokens:
stmt  if expr then stmt
| if expr then stmt else stmt

expr  term relop term
| term
term  id
| number
2. The next step is to formalize the patterns: 3. We also need to handle whitespaces:
digit  [0-9]
ws  (blank | tab | newline)+
Digits  digit+
number  digit(.digits)? (E[+-]? Digit)?
letter  [A-Za-z_]
id  letter (letter|digit)*
If  if
Then  then
Else  else
Relop  < | > | <= | >= | = | <> 41
Transition Diagram
Lexemes Token Names Attribute Value
Any ws - -
if if -
then then -
else else -
Any id id Pointer to table entry
Any number number Pointer to table entry
< relop LT
<= relop LE
= relop EQ
<> relop NE
> relop GT
>= relop GE

Fig. Transition diagram for relop


Table: Tokens, their patterns, and attribute values

Fig. Transition diagram Transition diagram for reserved words and identifiers 42
Finite Automata
 The lexical analyzer tools use finite automata, at the heart of the transition, to convert the

input program into a lexical analyzer .


 These are essentially graphs, like transition diagrams, with a few differences:

1. Finite automata are recognizers ; they simply say "yes" or "no" about each
possible input string.
2. Finite automata come in two flavors :
A. Nondeterministic finite automata (NFA) have no restrictions on the labels of
their edges . A symbol can label several edges out of the same state, and
Ɛ, the empty string, is a possible label.
B. Deterministic finite automata (DFA) have, for each state, and for each
symbol of its input alphabet exactly one edge with that symbol leaving
that state.
 Both deterministic and nondeterministic finite automata are capable of rec­ognizing

the same languages .


 In fact these languages are exactly the same languages , called the regular
44
Finite Automata State Graphs
A state Example 1: A finite automaton that accepts
only “1” 1

The start state


(Initial State)
Example 2: A finite automaton accepting any
number of 1’s followed by a single 0
Accepting state
(Final State) 1

0
a
A Transition

Q: Check that “1110” is accepted but “110…”


is not?

45
Example
b b
1) q1 c q2 a q4

c c
a b

c q3 a A DFA that can accept the strings which begin


with a or b, or begin with c and contain at most
b
one a.

2)

A DFA accepting (a|b) * abb


53
JavaCC- A Lexical Analyzer and Parser Generator

 Java Compiler Compiler (JavaCC) is the most popular Lexical analyzer and parser

generator for use with Java applications.

 In addition to this, JavaCC provides other standard capabilities related to parser

generation such as tree building (via a tool called JJTree included with JavaCC),

actions, debugging, etc.

 JavaCC takes a set of regular expressions as input that describe tokens

 Creates a DFA that recognizes the input set of tokens, and

 Then creates a Java class to implement that DFA

 JavaCC works with any Java VM version 1.2 or greater.

54

Flow for using JavaCC

 JavaCC is a “top-down” parser generator.


 Some parser generators (such as yacc , bison, and JavaCUP) need a separate
lexical-analyzer generator.
 With JavaCC, you can specify the tokens within the parser generator.
55
Structure of JavaCC File
 Input files to JavaCC have the extension “.jj”
 A sample example simple.jj file is shown on next slide # 50 and 51
 Several tokens can be defined in the same TOKEN block, with the rules
separated by a “│”. (see example from slide # 50)
 By convention, token names are in all UPPERCASE.
 When JavaCC runs on the file simple.jj, JavaCC creates a class
simpleTokenManager, which contains a method getNextToken().
 The getNextToken() method implements the DFA that recognizes the tokens
described by the regular expressions
 Every time getNextToken is called, it finds the rule whose regular expression
matches to the next sequence of characters in the input file, and returns the token
for that rule
56
Contd…
 Inputfile file is used as input file for simple.jj code, as shown on slide number

52.

 When there is more than one rule that matches the input, JavaCC uses the

following strategy:

1. Always match to the longest possible string

2. If two different rules match the same string, use the rule that appears

first in the .jj file

 For example, the “else” string matches both the ELSE and IDENTIFIER rules,

getNextToken() will return the ELSE token

 But “else21” matches to the IDENTIFIER token only 57


Simple.jj
<INNER_COMMENT>
SKIP:
{
PARSER_BEGIN(simple) <~[]>
public class simple |<"*/">
{ {
} count--;
PARSER_END(simple) if(count==0); SwitchTo(DEFAULT);
TOKEN_MGR_DECLS: }
{ }
public static int count=0;
} TOKEN:
SKIP: {
{ <ELSE:"else">
<" "> |<FOR:"for">
|<"\n"> |<AND:"and">
|<"\t"> |<CLASS:"class">
|<"\r"> |<PUBLIC:"public">
|<"\b"> |<PROTECTED:"protected">
|<"//"(~["\n"])*"\n"> |<PRIVATE:"private">
|<"/*"> {count++;} : |<DO:"do">
INNER_COMMENT |<IF:"if">
} |<WHILE:"while">
<INNER_COMMENT> |<INT:"int">
SKIP: |<FLOAT:"float">
{ |<CHAR:"char">
<"/*">{count++;}:INNER_COMMENT |<VOID:"void">
} } 58
Contd…
TOKEN: TOKEN:
{ {
<PLUS:"+"> <IDENTIFIERS:["a"-"z","A"-"Z"]
|<MINUS:"-"> (["a"-"z","A"-"Z","0"-"9"])*>
|<TIMES:"*"> |<INTEGER_LITERAL:(["0"-"9"])+>
|<DIVIDE:"/"> }
|<SEMICOLON:";"> void expression():
|<LEFT_PARENTHESIS:"("> {}
|<RIGHT_PARENTHESIS:")"> {
|<LEFT_SQUARE_BRACKET:"["> term()((<PLUS>|<MINUS>) term())*
|<RIGHT_SQUARE_BRACKET:"]"> }
|<LEFT_BRACE:"{">
|<RIGHT_BRACE:"}"> void term():
|<DOT:"."> {}
|<EQUAL_TO:"=="> {
|<NOT_EQUAL_TO:"!="> factor() ((<TIMES>|<DIVIDE>) factor())*
|<LESS_THAN:"<"> }
|<GREATER_THAN:">">
|<LESS_THAN_OR_EQUAL_TO:"<="> void factor():
|<GREATER_THAN_OR_EQUAL_TO:">="> {}
|<ASSIGNMENT:"="> {
|<LOGICAL_AND:"&&"> <INTEGER_LITERAL>|<IDENTIFIERS>
|<LOGICAL_OR:"||"> }
|<NOT:"!"> ||
|<DOLLAR:"$"> 59
}
Inputfile
/* this is an input file for simple.jj file
Prepared By:
Dileep Kumar
Date: 02/02/2014
********************************************* */
public class testLA
{
int y=7;
float w+=2;
char z=t*8;
int x,y=2,z+=3;
if(x>0)
{
sum/=x;
}
else if(x==0)
{
sum=x;
}
else {}
}
while(x>=n)
{
float sum=2*3+4-6;
} 60
}
Tokens in JavaCC
 When we run JavaCC on the input file simple.jj, JavaCC creates several files
to implement a lexical analyzer

1. simpleTokenManager.Java: to implement the token manager class

2. simpleConstants.Java: an interface that defines a set of constants

3. Token.Java: describes the tokens returned by getNextToken()

4. In addition to these, the following additional files are also created:


 simple.Java,
 ParseException.Java,
 SimpleCharStream.Java,
 TokenMgrError.Java

61
Using the generated TokenManager in JavaCC
 To create a Java program that uses the lexical analyzer created by JavaCC, you need
to instantiate a variable of type simpleTokenManager, where simple is the name of
your .jj file.
 The constructor for simpleTokenManager requires an object of type
SimpleCharStream.
 The constructor for SimpleCharStream requires a standard Java.io.InputStream.
 Thus, you can use the general lexical analyzer as follows:
Token t;
simpleTokenManager tm;
Java.io.InputStream infile;
tm = new simpleTokenManager(new SimpleCharStream(infile));
infile = new Java.io.FileInputstream(“Inputfile.txt”);
t = tm.getNextToken();
while (t.kind != simpleConstants.EOF)
{
/* Process t */ 62
Thank
Thank You
You ...
...
?

63

You might also like