This repository has been archived by the owner on Apr 29, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
pyre.py
495 lines (413 loc) · 14.1 KB
/
pyre.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
#! /usr/bin/env python
# -*- coding: cp1252 -*-
import ply.lex as lex
import ply.yacc as yacc
import re
import sys
# Lexing
tokens = ('RANG', 'LANG', 'SOL', 'LOWBAR',
'NUMBER', 'ID', 'AST', 'QUEST', 'COMMA', 'VERT',
'LPAR', 'RPAR', 'LSQB', 'RSQB', 'LCUB', 'RCUB',
'PLUS', 'MINUS', 'ALPHA', 'NALPHA',
'EQUALS', 'COLON', 'RARR', 'LARR', 'EQUIV')
t_ignore = ' \t'
t_RANG = r'>'
t_LANG = r'<'
t_SOL = r'\/'
t_LOWBAR = r'_'
t_ID = r"[a-zA-Z0-9._']+"
t_LPAR = r'\('
t_RPAR = r'\)'
t_LSQB = r'\['
t_RSQB = r'\]'
t_LCUB = r'\{'
t_RCUB = r'\}'
t_ALPHA = r'@'
t_NALPHA = r'-@'
t_AST = r'\*'
t_QUEST = r'\?'
t_COMMA = r','
t_VERT = r'\|'
t_EQUALS = r'='
t_COLON = r':'
t_RARR = r'=>'
t_LARR = r'<='
t_EQUIV = r'=='
def t_PLUS(t):
r'\+'
t.value = True
return t
def t_MINUS(t):
r'-'
t.value = False
return t
def t_NUMBER(t):
r'\d+'
t.value = int(t.value)
return t
def t_newline(t):
r'(\n|\r|\f)+'
t.lexer.lineno += len(t.value)
def t_error(t):
"""Print an error when an illegal character is found."""
sys.stderr.write("Warning: Illegal character '%s'\n" % t.value[0])
t.lexer.skip(1)
lexer = lex.lex(debug=True)
# Phonemes and features
symbols = {}
class Phoneme:
"""
A wrapper class for a dictionary from features to Booleans.
A phoneme is a set of signed features. A signed feature is a feature (for
example, voice) paired with a sign (+ or -). Unspecified features (for
example, the place of articulation of the Japanese moraic n) are specified
by their absence from the phoneme.
The purpose of this wrapper class is to maintain the invariants of
phonemes, which are specified by constraints.
"""
def __init__(self, features=dict(), plus=set(), minus=set()):
"""
Create a new phoneme.
Optional arguments:
features : a dictionary from features to Booleans
plus : a set of positive features
minus : a set of negative features
"""
self.features = features.copy()
self.features.update({f: True for f in plus})
self.features.update({f: False for f in minus})
def __repr__(self):
"""Return a formal representation of this phoneme as a string."""
return 'Phoneme(%s)' % self.features
def __str__(self):
"""
Return an informal representation of this phoneme as a string.
Features are listed in alphabetical order with a '+' or '-' prefix as
appropriate.
"""
signed_strings = []
for key in sorted(self.features.keys()):
if self[key]: signed_strings.append('+%s' % key)
else: signed_strings.append('-%s' % key)
return '[%s]' % ' '.join(signed_strings)
def __hash__(self):
"""Return a hash code for this phoneme."""
return (hash(tuple(self.features.keys())) ^
hash(tuple(self.features.values())))
def __eq__(self, other):
"""
Return whether this phoneme's features equal another's.
Arguments:
other : the object to test equality against
"""
if not isinstance(other, Phoneme): return False
if len(self.features) != len(other.features): return False
return self <= other
def __ne__(self, other):
"""
Return whether this phoneme does not equal an object.
Arguments:
other : the object to test inequality against
"""
return not self == other
def __le__(self, other):
"""
Return whether this phoneme is a subset of another phoneme.
A phoneme is a subset of another if every feature of the one appears in
the other with the same sign.
Arguments:
other : the phoneme which might be a superset
"""
if not isinstance(other, Phoneme):
raise TypeError('can only compare to a Phoneme')
for feature in self.features.keys():
if feature in other.features:
if self[feature] != other[feature]:
return False
else:
return False
return True
def __lt__(self, other):
return self <= other and len(self.features) != len(other.features)
def __ge__(self, other):
if not isinstance(other, Phoneme):
raise TypeError('can only compare to a Phoneme')
for feature in other.features.keys():
if feature in self.features:
if self[feature] != other[feature]:
return False
else:
return False
return True
def __gt__(self, other):
return other <= self and len(self.features) != len(other.features)
def __getitem__(self, key):
"""
Return the sign of a feature as a Boolean, or None if it is absent.
Arguments:
key : the feature whose sign is wanted
"""
try:
return self.features[key]
except KeyError:
return None
def contradicts(self, other):
"""
Return whether this phoneme contradicts another.
Phonemes contradict each other when any feature of one appears in the
other with a different sign.
Arguments:
other : the phoneme to compare against
"""
return self.contradictsi(other.features)
def contradictsi(self, features):
"""
Return whether this phoneme contradicts a set of signed features.
A phoneme contradicts a set of signed features when any feature of the
phoneme appears in the set with a different sign.
Arguments:
features : the dictionary of signed features
"""
for feature in self.features.keys():
if feature in features and self[feature] != features[feature]:
return True
return False
def edit(self, other):
"""
Add another phoneme's signed features, unless any contradict.
Return this phoneme.
Arguments:
other : the phoneme to get the new signed features from
"""
return self.editi(other.features)
def editi(self, features):
"""
Add some signed features, unless any contradict.
Return this phoneme.
Arguments:
features : the dictionary of signed features
"""
if self.contradictsi(features):
sys.stderr.write("Warning: Inconsistent feature update\n")
else:
self.updatei(features)
return self
def update(self, other):
"""
Add another phoneme's signed features, overwriting in case of conflict.
Return this phoneme.
Arguments:
other : the phoneme to get the new signed features from
"""
return self.updatei(other.features)
def updatei(self, features):
"""
Add some signed features, overwriting in case of conflict.
Return this phoneme.
Arguments:
features : the dictionary of signed features
"""
new = self.copy()
new.features.update(features)
if new.follows_constraints(): self.features = new.features
return self
def copy(self):
"""Return a copy of this phoneme."""
return Phoneme(self.features)
def follows_constraints(self):
"""
Return whether this phoneme's features do not violate any constraints.
As a side effect, update this phoneme with any features implied by
constraints.
"""
for constraint in constraints:
if constraint <= self:
if self.contradicts(constraints[constraint]):
sys.stderr.write('Error: the phoneme %s violates that '
'constraint!\n' % self)
return False
else:
for feature in constraints[constraint].features:
self.features.update(
{feature: constraints[constraint][feature]})
return True
def p_error(p):
"""Fail, and go to the next line."""
try:
sys.stderr.write('Syntax error on line %d on token %s\n' %
(p.lexer.lineno, p.type))
except AttributeError:
sys.stderr.write('Unexpected EOL\n')
while True:
tok = yacc.token()
if not tok: break
yacc.restart()
def p_line_new_features_ambiguous(p):
'line : new_symbols COLON new_symbols'
# None : Set(String) Constant Set(String)
features = {f: True for f in p[1]}
for symbol in p[3]:
if not symbol in symbols: symbols[symbol] = Phoneme()
symbols[symbol].editi(features)
print('%s = %s' % (symbol, symbols[symbol]))
def p_line_new_features(p):
'line : features COLON new_symbols'
# None : Phoneme Constant Set(String)
for symbol in p[3]:
if not symbol in symbols: symbols[symbol] = Phoneme()
symbols[symbol].edit(p[1])
print('%s = %s' % (symbol, symbols[symbol]))
def p_line_new_phonemes_ambiguous(p):
'line : new_symbols EQUALS new_symbols'
# None : Set(String) Constant Set(String)
features = {f: True for f in p[3]}
for symbol in p[1]:
if not symbol in symbols: symbols[symbol] = Phoneme()
symbols[symbol].editi(features)
print('%s = %s' % (symbol, symbols[symbol]))
def p_line_new_phonemes(p):
'line : new_symbols EQUALS features'
# None : Set(String) Constant Phoneme
for symbol in p[1]:
if not symbol in symbols: symbols[symbol] = Phoneme()
symbols[symbol].edit(p[3])
print('%s = %s' % (symbol, symbols[symbol]))
def p_new_symbols_base(p):
'new_symbols : ID'
# Set(String) : String
p[0] = set([p[1]])
def p_new_symbols_recursive(p):
'new_symbols : new_symbols ID'
# List(String) : Set(String) String
p[1].add(p[2])
p[0] = p[1]
def p_features_base(p):
'''
features : phoneme
| feature
'''
# Phoneme : Phoneme
p[0] = p[1]
def p_features_recursive(p):
'''
features : features phoneme
| features feature
'''
# Phoneme : Phoneme Phoneme
p[1].update(p[2])
p[0] = p[1]
def p_features_recursive_ambiguous(p):
'''
features : new_symbols feature
| new_symbols phoneme
'''
# Phoneme : Set(String) Phoneme
p[2].updatei({f: True for f in p[1]})
p[0] = p[2]
def p_features_recursive_ambiguous(p):
'features : features ID'
# Phoneme : Phoneme String
p[1].updatei({p[2]: True})
p[0] = p[1]
def p_feature(p):
'''
feature : PLUS ID
| MINUS ID
'''
# Phoneme : Boolean String
p[0] = Phoneme({p[2]: p[1]})
def p_phoneme(p):
'phoneme : SOL valid_phoneme SOL'
# Phoneme : Constant Phoneme Constant
p[0] = p[2]
def p_valid_symbol(p):
'valid_phoneme : ID'
# Phoneme : String
if not p[1] in symbols:
sys.stderr.write('Error: No such phoneme /%s/\n' % p[1])
raise SyntaxError
p[0] = symbols[p[1]].copy()
# Constraints
constraints = {}
def add_constraint(key, value):
"""
Add a new key-value pair to the dictionary of constraints.
A constraint is only added if it is self-consistent, it is not redundant
with any previous constraints, and it does not contradict any previous
constraints. If any previous constraint is found to be redundant with it,
the previous one is replaced.
Arguments:
key : the antecedent phoneme
value : the consequent phoneme
"""
if not key.contradicts(value):
worthwhile = True
for antecedent in constraints.copy():
consequent = constraints[antecedent]
#if antecedent <= key and value.contradicts(consequent):
# sys.stderr.write('%s <= %s and %s.contradicts(%s)\n' %
# (antecedent, key, value, consequent))
# worthwhile = False
# break
if antecedent <= key and value <= consequent:
#print('%s <= %s and %s <= %s' %
# (antecedent, key, value, consequent))
worthwhile = False
break
if key <= antecedent and consequent <= value:
#print('%s <= %s and %s <= %s' %
# (key, antecedent, consequent, value))
del constraints[antecedent]
if worthwhile:
if key in constraints: constraints[key].edit(value)
else: constraints[key] = value
def p_implication_ambiguous_lr(p):
'line : new_symbols RARR new_symbols'
# None : Set(String) Constant Set(String)
add_constraint(Phoneme({f: True for f in p[1]}),
Phoneme({f: True for f in p[3]}))
print(constraints)
def p_implication_ambiguous_l(p):
'line : new_symbols RARR features'
# None : Set(String) Constant Phoneme
add_constraint(Phoneme({f: True for f in p[1]}), p[3])
print(constraints)
def p_implication_ambiguous_r(p):
'line : features RARR new_symbols'
# None : Phoneme Constant Set(String)
add_constraint(p[1], Phoneme({f: True for f in p[3]}))
print(constraints)
def p_implication(p):
'line : features RARR features'
# None : Phoneme Constant Phoneme
add_constraint(p[1], p[3])
print(constraints)
def p_converse_implication_ambiguous_lr(p):
'line : new_symbols LARR new_symbols'
# None : Set(String) Constant Set(String)
add_constraint(Phoneme({f: True for f in p[3]}),
Phoneme({f: True for f in p[1]}))
print(constraints)
def p_converse_implication_ambiguous_l(p):
'line : new_symbols LARR features'
# None : Set(String) Constant Phoneme
add_constraint(p[3], Phoneme({f: True for f in p[1]}))
print(constraints)
def p_converse_implication_ambiguous_r(p):
'line : features LARR new_symbols'
# None : Phoneme Constant Set(String)
add_constraint(Phoneme({f: True for f in p[3]}), p[1])
print(constraints)
def p_converse_implication(p):
'line : features LARR features'
# None : Phoneme Constant Phoneme
add_constraint(p[3], p[1])
print(constraints)
# Running the program
parser = yacc.yacc(start='line')
while True:
try: s = raw_input('> ')
except EOFError: break
if not s: continue
result = parser.parse(s)
print(result)