-
Notifications
You must be signed in to change notification settings - Fork 17
/
Copy pathgraph.lua
433 lines (381 loc) · 14.2 KB
/
graph.lua
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
local usage = require 'argcheck.usage'
local utils = require 'argcheck.utils'
local function argname2idx(rules, name)
for idx, rule in ipairs(rules) do
if rule.name == name then
return idx
end
end
error(string.format('invalid defaulta name <%s>', name))
end
local function table2id(tbl)
-- DEBUG: gros hack de misere
return tostring(tbl):match('0x([^%s]+)')
end
local function func2id(func)
-- DEBUG: gros hack de misere
return tostring(func):match('0x([^%s]+)')
end
local function rules2maskedrules(rules, rulesmask, rulestype, iscall)
local maskedrules = {}
for ridx=1,#rulesmask do
local rule = utils.duptable(rules[ridx])
rule.__ridx = ridx
if not iscall then -- do not mess up the name for a call
if rulestype == 'O' then
rule.name = nil
elseif rulestype == 'M' and ridx == 1 then -- self?
rule.name = nil
end
end
local rulemask = rulesmask:sub(ridx,ridx)
if rulemask == '1' then
table.insert(maskedrules, rule)
elseif rulemask == '2' then
elseif rulemask == '3' and rulestype == 'O' then
rule.type = 'nil'
rule.check = nil
table.insert(maskedrules, rule)
end
end
return maskedrules
end
local function rules2defaultrules(rules, rulesmask, rulestype)
local defaultrules = {}
for ridx=1,#rulesmask do
local rule = utils.duptable(rules[ridx])
rule.__ridx = ridx
if rulestype == 'O' then
rule.name = nil
elseif rulestype == 'M' and ridx == 1 then -- self?
rule.name = nil
end
local rulemask = rulesmask:sub(ridx,ridx)
if rulemask == '1' then
elseif rulemask == '2' then
table.insert(defaultrules, rule)
elseif rulemask == '3' then
if rule.default or rule.defaulta or rule.defaultf then
table.insert(defaultrules, rule)
end
end
end
return defaultrules
end
local ACN = {}
function ACN.new(typename, name, check, rules, rulesmask, rulestype)
assert(typename)
local self = {}
setmetatable(self, {__index=ACN})
self.type = typename
self.name = name
self.check = check
self.rules = rules
self.rulesmask = rulesmask
self.rulestype = rulestype
self.next = {}
return self
end
function ACN:add(node)
table.insert(self.next, node)
end
function ACN:match(rules)
local head = self
for idx=1,#rules do
local rule = rules[idx]
local matched = false
for _,child in ipairs(head.next) do
if child.type == rule.type
and child.check == rule.check
and child.name == rule.name then
head = child
matched = true
break
end
end
if not matched then
return head, idx-1
end
end
return head, #rules
end
function ACN:hasruletype(ruletype)
local hasruletype
self:apply(function(self)
if self.rulestype == ruletype then
hasruletype = true
end
end)
return hasruletype
end
function ACN:addpath(rules, rulesmask, rulestype) -- 'O', 'N', 'M'
-- DEBUG: on peut aussi imaginer avoir d'abord mis
-- les no-named, et ensuite les named!!
assert(rules)
assert(rulesmask)
assert(rulestype)
local maskedrules = rules2maskedrules(rules, rulesmask, rulestype, false)
if rulestype == 'N' then
table.insert(maskedrules, 1, {type='table'})
end
if rulestype == 'M' then
table.insert(maskedrules, 2, {type='table'})
end
local head, idx = self:match(maskedrules)
if idx == #maskedrules then
-- check we are not overwriting something here
if not rules.force and head.rules and rules ~= head.rules then
error('argcheck rules led to ambiguous situations')
end
head.rules = rules
head.rulesmask = rulesmask
head.rulestype = rulestype
end
for idx=idx+1,#maskedrules do
local rule = maskedrules[idx]
local node = ACN.new(rule.type,
rule.name,
rule.check,
idx == #maskedrules and rules or nil,
idx == #maskedrules and rulesmask or nil,
idx == #maskedrules and rulestype or nil)
head:add(node)
head = node
end
-- special trick: mark self
if rulestype == 'M' then
local head, idx = self:match({maskedrules[1]}) -- find self
assert(idx == 1, 'internal bug, please report')
head.isself = true
end
end
function ACN:id()
return table2id(self)
end
function ACN:print(txt)
local isroot = not txt
txt = txt or {'digraph ACN {'}
table.insert(txt, 'edge [penwidth=.3 arrowsize=0.8];')
table.insert(txt, string.format('id%s [label="%s%s%s%s" penwidth=.1 fontsize=10 style=filled fillcolor="%s"];',
self:id(),
self.type,
self.isself and '*' or '',
self.check and ' <check>' or '',
self.name and string.format(' (%s)', self.name) or '',
self.rules and '#aaaaaa' or '#eeeeee'))
for _,child in ipairs(self.next) do
child:print(txt) -- make sure its id is defined
table.insert(txt, string.format('id%s -> id%s;',
self:id(),
child:id()))
end
if isroot then
table.insert(txt, '}')
txt = table.concat(txt, '\n')
return txt
end
end
function ACN:generate_ordered_or_named(code, upvalues, rulestype, depth)
depth = depth or 0
-- no need to go deeper if no rules found later
if not self:hasruletype(rulestype) then
return
end
if depth > 0 then
local argname =
(rulestype == 'N' or rulestype == 'M')
and string.format('args.%s', self.name)
or string.format('select(%d, ...)', depth)
if self.check then
upvalues[string.format('check%s', func2id(self.check))] = self.check
end
if self.type == 'nil' and (rulestype == 'N' or rulestype == 'M') then
table.insert(code, string.format('%sif istype(%s, "%s")%s then',
string.rep(' ', depth),
argname,
self.type,
self.check and string.format(' and check%s(%s)', func2id(self.check), argname) or ''))
else
table.insert(code, string.format('%sif narg > 0 and istype(%s, "%s")%s then',
string.rep(' ', depth),
argname,
self.type,
self.check and string.format(' and check%s(%s)', func2id(self.check), argname) or ''))
table.insert(code, string.format('%s narg = narg - 1', string.rep(' ', depth)))
end
end
if self.rules and self.rulestype == rulestype then
local rules = self.rules
local rulesmask = self.rulesmask
local id = table2id(rules)
table.insert(code, string.format(' %sif narg == 0 then', string.rep(' ', depth)))
-- 'M' case (method: first arg is self)
if rulestype == 'M' then
rules = utils.duptable(self.rules)
table.remove(rules, 1) -- remove self
rulesmask = rulesmask:sub(2)
end
-- func args
local argcode = {}
for ridx, rule in ipairs(rules) do
if rules.pack then
table.insert(argcode, string.format('%s=arg%d', rule.name, ridx))
else
table.insert(argcode, string.format('arg%d', ridx))
end
end
-- passed arguments
local maskedrules = rules2maskedrules(rules, rulesmask, rulestype, true)
for argidx, rule in ipairs(maskedrules) do
local argname =
(rulestype == 'N' or rulestype == 'M')
and string.format('args.%s', rule.name)
or string.format('select(%d, ...)', argidx)
table.insert(code, string.format(' %slocal arg%d = %s',
string.rep(' ', depth),
rule.__ridx,
argname))
end
-- default arguments
local defaultrules = rules2defaultrules(rules, rulesmask)
local defacode = {}
for _, rule in ipairs(defaultrules) do
local defidx = rulestype == 'M' and rule.__ridx+1 or rule.__ridx
if rule.default ~= nil then
table.insert(code, string.format(' %slocal arg%d = arg%s_%dd', string.rep(' ', depth), rule.__ridx, id, defidx))
upvalues[string.format('arg%s_%dd', id, defidx)] = rule.default
elseif rule.defaultf then
table.insert(code, string.format(' %slocal arg%d = arg%s_%df()', string.rep(' ', depth), rule.__ridx, id, defidx))
upvalues[string.format('arg%s_%df', id, defidx)] = rule.defaultf
elseif rule.opt then
table.insert(code, string.format(' %slocal arg%d', string.rep(' ', depth), rule.__ridx))
elseif rule.defaulta then
table.insert(defacode, string.format(' %slocal arg%d = arg%d', string.rep(' ', depth), rule.__ridx, argname2idx(rules, rule.defaulta)))
end
end
if #defacode > 0 then
table.insert(code, table.concat(defacode, '\n'))
end
if rules.pack then
argcode = table.concat(argcode, ', ')
if rulestype == 'M' then
argcode = string.format('self, {%s}', argcode)
else
argcode = string.format('{%s}', argcode)
end
else
if rulestype == 'M' then
table.insert(argcode, 1, 'self')
end
argcode = table.concat(argcode, ', ')
end
if rules.call and not rules.quiet then
argcode = string.format('call%s(%s)', id, argcode)
upvalues[string.format('call%s', id)] = rules.call
end
if rules.quiet and not rules.call then
argcode = string.format('true%s%s', #argcode > 0 and ', ' or '', argcode)
end
if rules.quiet and rules.call then
argcode = string.format('call%s%s%s', id, #argcode > 0 and ', ' or '', argcode)
upvalues[string.format('call%s', id)] = rules.call
end
table.insert(code, string.format(' %sreturn %s', string.rep(' ', depth), argcode))
table.insert(code, string.format(' %send', string.rep(' ', depth)))
end
for _,child in ipairs(self.next) do
child:generate_ordered_or_named(code, upvalues, rulestype, depth+1)
end
if depth > 0 then
if self.type ~= 'nil' or (rulestype ~= 'N' and rulestype ~= 'M') then
table.insert(code, string.format('%s narg = narg + 1', string.rep(' ', depth)))
end
table.insert(code, string.format('%send', string.rep(' ', depth)))
end
end
function ACN:apply(func)
func(self)
for _,child in ipairs(self.next) do
child:apply(func)
end
end
function ACN:usage(...)
local txt = {}
local history = {}
self:apply(
function(self)
if self.rules and not history[self.rules] then
history[self.rules] = true
table.insert(txt, usage.usage(true, self.rules))
end
end
)
return string.format(
"%s\n%s\n",
table.concat(txt, '\n\nor\n\n'),
usage.usage(false, self, ...)
)
end
function ACN:generate(upvalues)
assert(upvalues, 'upvalues table missing')
local code = {}
table.insert(code, 'return function(...)')
table.insert(code, ' local usage = require "argcheck.usage"')
table.insert(code, ' local narg = select("#", ...)')
self:generate_ordered_or_named(code, upvalues, 'O')
if self:hasruletype('N') then -- is there any named?
local selfnamed = self:match({{type='table'}})
assert(selfnamed ~= self, 'internal bug, please report')
table.insert(code, ' if select("#", ...) == 1 and istype(select(1, ...), "table") then')
table.insert(code, ' local args = select(1, ...)')
table.insert(code, ' local narg = 0')
table.insert(code, ' for k,v in pairs(args) do')
table.insert(code, ' narg = narg + 1')
table.insert(code, ' end')
selfnamed:generate_ordered_or_named(code, upvalues, 'N')
table.insert(code, ' end')
end
for _,head in ipairs(self.next) do
if head.isself then -- named self method
local selfnamed = head:match({{type='table'}})
assert(selfnamed ~= head, 'internal bug, please report')
if head.check then
upvalues[string.format('check%s', func2id(head.check))] = head.check
end
table.insert(code,
string.format(' if select("#", ...) == 2 and istype(select(2, ...), "table") and istype(select(1, ...), "%s")%s then',
head.type,
head.check and string.format(' and check%s(select(1, ...))', func2id(head.check)) or '')
)
table.insert(code, ' local self = select(1, ...)')
table.insert(code, ' local args = select(2, ...)')
table.insert(code, ' local narg = 0')
table.insert(code, ' for k,v in pairs(args) do')
table.insert(code, ' narg = narg + 1')
table.insert(code, ' end')
selfnamed:generate_ordered_or_named(code, upvalues, 'M')
table.insert(code, ' end')
end
end
for upvaluename, upvalue in pairs(upvalues) do
table.insert(code, 1, string.format('local %s', upvaluename))
end
table.insert(code, ' assert(istype)') -- keep istype as an upvalue
table.insert(code, ' assert(graph)') -- keep graph as an upvalue
local quiet = true
self:apply(
function(self)
if self.rules and not self.rules.quiet then
quiet = false
end
end
)
if quiet then
table.insert(code, ' return false, usage.render(graph:usage(...))')
else
table.insert(code, ' error(string.format("%s\\ninvalid arguments!", usage.render(graph:usage(...))))')
end
table.insert(code, 'end')
return table.concat(code, '\n')
end
return ACN