Sunday, November 29, 2015

Ningbonese Consonants

In general, Wu Chinese dialects have a similar set of initial consonants as Mandarin, with one major difference: Wu Chinese preserves the voiced initial consonants of Old Chinese -- so there's a distinction between 白 (voiced) and 百 (unvoiced). Additionally, Wu Chinese does not have retroflex consonants (Mandarin zh/ch/sh/r).

It may be difficult for learners to distinguish the voiced and voiceless un-aspirated consonants; it's helpful to note that syllables with voiced initial consonants always have a light (阳) tone, while syllables with voiceless initial syllables always have dark (阴) tones.

There are three groups of three stop consonants: voiceless un-aspirated, aspirated, and voiced p, t, and k:
IPAExampleEnglish
pp八 paheight
ph漂亮 phio lianbeautiful
bb朋友 ban yeufriend
tt多少 tou shiohow much
th天亮 thi nyanmorning
dd头发 deu fahhair
kk交关 chio kuaevery
kh困觉 khuen kausleep
gg番茄 fae gatomato

There are also two groups: "c/ch" is like Mandarin "j/q", and is usually followed by an "i" sound; "ts/tsh" is like Mandarin "z/c".

tsts钟头 tson deuhour
tshtsʰ出 tshoehout
dzdz茶 dzotea
c今末 cih mahtoday
chtɕʰ去 chiugo
j其 jihim/her

There are four groups of fricative consonants, each with voiced and unvoiced versions. It can be particularly difficult to hear the voiced glottal fricative "gh" and the voiced palatal fricative "zh", since these sounds do not appear in English or Mandarin.

ff飞机 fi ciairplane
vv不好 vah haunot good
hh好 haugood
ghɦ后 gheuback
ss水 syuwater
zz辰光 zoen kuaontime
shɕ晓得 shio tahknow
zhʑ前 zhifront

Finally, there are the sonorant consonants -- unlike Mandarin, this includes a velar nasal (ng) and a palatal nasal (ny).

mm明朝 min ciotomorrow
nyȵ人 nyinperson
ngŋ我 ngome
nn侬 nouyou
ll来 lecome

Reference: Wu-chinese.com.

Saturday, November 21, 2015

Giving values to expressions in yacc

In the last post, I started to use yacc, but in that example, when rules were matched, the action for each message was just to print out a message; the tokens were not combined or evaluated in any way.

Inside an action block after a grammar rule expr : X Y Z, $$ is the value of the left-hand side (the output when that rule is used to reduce a sequence of expressions), and expressions on the right hand side are in the variables $1, $2, $3, etc.

For example, in our logic expression grammar we can have:

expr    : NOT expr { $$ = ! $2 }
        | expr AND expr { $$ = ($1 && $3); }
        | expr OR expr { $$ = ($1 || $3); }
        | LPAREN expr RPAREN { $$ = $2; }
        | TRUE { $$ = 1; }
        | FALSE { $$ = 0; }
        ;

Each alternative (separated by |) can have an action associated with it; each of these rules can combine the right hand side values in a different way to set $$.

With this, we can now parse and evaluate complex expressions:

File: logic.y
%{
#include 
%}

%token TRUE FALSE AND OR NOT LPAREN RPAREN
%left OR
%left AND
%left NOT

%%

start   : expr {
          if ($1) {
            printf("true\n");
          } else {
            printf("false\n");
          }
        }

expr    : NOT expr { $$ = ! $2; }
        | expr AND expr { $$ = ($1 && $3); }
        | expr OR expr { $$ = ($1 || $3); }
        | LPAREN expr RPAREN { $$ = $2; }
        | TRUE { $$ = 1; }
        | FALSE { $$ = 0; }
        ;

%%

extern FILE *yyin;

int main() {
  do {
    printf("> ");
    yyparse();
  } while (!feof(yyin));
}

yyerror(s) char *s; {
  fprintf(stderr, "yyerror called with: %s\n", s);
}

The %token section at the top, as before, specifies the tokens and allows yacc to generate a header file that's also used by the lexer. the %left rules above specify operator precedence order and associativity so that the rules are not ambiguous.

The first rule, start has one action associated with it, which is to print out the value of a parsed expression. In this grammar, there are several possible patterns that can comprise an expr; all of these rules are recursive except for true and false, which have primitive values.

In this example, the main function now prints a prompt, reads a sequence of tokens and parses, and repeats until EOF (ctrl-D on the terminal) is entered.

Example:
$ ./calc
> false and false and false and false 
false
> true and (false or not false)
true
> true and
yyerror called with: syntax error
> 

Sunday, November 15, 2015

Combining lex and yacc

In the previous post, I posted an example of a simple lexer with lex, where the action for each rule was to print out what rule it matched.

When lex is used with yacc, however, each lex rule may just return an integer which corresponds to a given token type, defined in a header file output by yacc. For example:

File: logic.l
%{
#include "y.tab.h"
%}

%%

not { return NOT; }
or  { return OR; }
and { return AND; }
\(  { return LPAREN; }
\)  { return RPAREN; }
false { return FALSE; }
true  { return TRUE; }

\n { return 0; }  // End marker

. ;  // Ignore everything else.

%%

In this example, the header generated by yacc is included in the header section of the file; each of the rules just returns one of the token IDs defined in y.tab.h.

Of special note is the special token which has a return value of 0, which indicates the end of input.

The yacc file looks like this:

File: logic.y
%{
#include 
%}

%token TRUE FALSE AND OR NOT LPAREN RPAREN

%%

expr    : literal
        | unary
        | binary
        | parens
        { printf("Parsed expr.\n"); }
        ;

literal : TRUE
        | FALSE
        { printf("Parsed literal.\n"); }
        ;

unary   : NOT expr
        { printf("Parsed not expr.\n"); }
        ;

binary  : expr AND expr
        | expr OR expr
        { printf("Parsed binary expr.\n"); }
        ;

parens  : LPAREN expr RPAREN
        { printf("Parsed paren expr.\n"); }
        ;

%%

int main() {
  yyparse();
  printf("Called yyparse once; terminating.\n"); 
}

yyerror(s) char *s; {
  fprintf(stderr, "yyerror called with: %s\n", s);
}

As with the lex file, the yacc file has a header section which can contain C code, and then a sequence of rules. In this header section we can include libraries like stdio.h, which allows us to use printf below.

After the header section (delimited by %{ %}) we have a list of token types. Token type names are traditionally written in all upper-case, although this is just a convention.

In this example, expr, literal, unary, binary and parens all match a particular types of expressions. The first rule listed is the "start" symbol; if there is a sequence of tokens that can be parsed using the first rule after the "end marker" token (0), then the expression is considered parsed (accepted); otherwise there is a syntax error.

Below the list of rules we have some C code, which gets copied verbatim into the C source file that yacc outputs. The above lex and yacc file can be made into a working binary by running the commands:

$ lex logic.l
$ yacc -d logic.y
$ cc -c -o lex.yy.o lex.yy.c
$ cc -c -o y.tab.o y.tab.c
$ cc -o calc lex.yy.o y.tab.o -ly -ll

Lex by default produces lex.yy.c, as before; yacc produces y.tab.c, and since the option -d is passed, it also produces y.tab.h which contains the token definitions. Then lex.yy.c and y.tab.c can be separately compiled into object files, and these object files can be linked together into a binary.

To make this more convenient, it's a good idea to use a Makefile:

File: Makefile
all: calc

clean:
        rm *.c *.h *.o calc

calc: lex.yy.o y.tab.o
        cc -o calc lex.yy.o y.tab.o -ly -ll

lex.yy.o: lex.yy.c y.tab.h

y.tab.o: y.tab.c y.tab.h

y.tab.h y.tab.c: logic.y
        yacc -d logic.y

lex.yy.c: logic.l
        lex logic.l

Finally, we can run the program:

$ ./calc
true and (not false)
Parsed literal.
Parsed not expr.
Parsed paren expr.
Parsed expr.
Called yyparse once; terminating.

And if there's a syntax error (no complete expression before the end marker token (0)):

false and
Parsed literal.
yyerror called with: syntax error
Called yyparse once; terminating.

The next step would be to build some kind of parse tree and evaluate it.

Saturday, November 7, 2015

Starting to look at lex

Last week I came across a copy of lex & yacc and started to read it. For my first toy project to learn the content, I'd like to make some kind of a "logic calculator", which might, for example, behave like this:

> true and false
false
> false or not false
true
> a = true, b = false, c = false
> (b or not a) and c
false

The first step in this task is to make a lexer to tokenize the input -- to convert the input into a sequence of tokens. With lex, a lexer is defined in a special domain specific language. Example 1:

File: logic.l
%%

not { printf("NOT: %s\n", yytext); }
or  { printf("OR: %s\n", yytext); }
and { printf("AND: %s\n", yytext); }
\(  { printf("LPAREN: %s\n", yytext); }
\)  { printf("RPAREN: %s\n", yytext); }
false { printf("FALSE: %s\n", yytext); }
true  { printf("TRUE: %s\n", yytext); }

. ;  // Ignore everything else.

%%

Things to note: The lex file contains a rules section, marked at the start and end with %%. In the minimal example, a lex example can just contain a sequence of rules. Each rule consists of a regular expression followed by a C block which contains the code to execute when the lexer finds input that matches that regular expression. Inside of that C block, the string that matched the regular expression is available as yytext.

Once we have a lex file like the one above, we can use lex to make a C source file from it:

$ lex logic.l

This creates a file called lex.yy.c (1791 lines!) which can then be compiled and run.

$ cc lex.yy.c -o lexer -ll

$ ./lexer
(true and false) ...
LPAREN: (
TRUE: true
AND: and
FALSE: false
RPAREN: )

^C

In this example, each of the rules in the lexer files prints some text to stdout and doesn't actually return any token type, but often when lex lexer is used inside a parser, the block of code for each of the rules will return an integer, which represents a token type.