File Coverage

blib/lib/Algorithm/ConstructDFA2.pm
Criterion Covered Total %
statement 144 149 96.6
branch 6 8 75.0
condition 1 2 50.0
subroutine 35 37 94.5
pod 7 8 87.5
total 193 204 94.6


line stmt bran cond sub pod time code
1             package Algorithm::ConstructDFA2;
2 2     2   131640 use strict;
  2         17  
  2         61  
3 2     2   10 use warnings;
  2         4  
  2         56  
4 2     2   49 use 5.024000;
  2         5  
5 2     2   1061 use Types::Standard qw/:all/;
  2         142691  
  2         26  
6 2     2   83724 use List::UtilsBy qw/sort_by nsort_by partition_by/;
  2         3359  
  2         162  
7 2     2   961 use List::MoreUtils qw/uniq/;
  2         15340  
  2         18  
8 2     2   3142 use Moo;
  2         17964  
  2         13  
9 2     2   4283 use Memoize;
  2         4109  
  2         96  
10 2     2   828 use Log::Any qw//;
  2         13945  
  2         42  
11 2     2   2649 use DBI;
  2         41955  
  2         129  
12 2     2   1279 use JSON;
  2         17208  
  2         13  
13              
14             our $VERSION = '0.06';
15              
16             has 'input_alphabet' => (
17             is => 'ro',
18             required => 1,
19             isa => ArrayRef[Int],
20             );
21              
22             has 'input_vertices' => (
23             is => 'ro',
24             required => 1,
25             isa => ArrayRef[Int],
26             default => sub { [] },
27             );
28              
29             has 'input_edges' => (
30             is => 'ro',
31             required => 1,
32             isa => ArrayRef[ArrayRef[Int]],
33             );
34              
35             has 'vertex_matches' => (
36             is => 'ro',
37             required => 1,
38             isa => CodeRef,
39             );
40              
41             has 'vertex_nullable' => (
42             is => 'ro',
43             required => 1,
44             isa => CodeRef,
45             );
46              
47             has 'storage_dsn' => (
48             is => 'ro',
49             required => 1,
50             isa => Str,
51             default => sub {
52             'dbi:SQLite:dbname=:memory:'
53             },
54             );
55              
56             has '_dbh' => (
57             is => 'ro',
58             required => 0,
59             writer => '_set_dbh',
60             );
61              
62             has 'dead_state_id' => (
63             is => 'ro',
64             required => 0,
65             isa => Int,
66             writer => '_set_dead_state_id',
67             );
68              
69             has '_log' => (
70             is => 'rw',
71             required => 0,
72             default => sub {
73             Log::Any->get_logger()
74             },
75             );
76              
77             has '_json' => (
78             is => 'rw',
79             required => 0,
80             default => sub {
81             JSON->new->canonical(1)->indent(0)->ascii(1)
82             },
83             );
84              
85             sub BUILD {
86 100     100 0 56663 my ($self) = @_;
87              
88             ###################################################################
89             # Create dbh
90              
91 100         908 $self->_log->debug("Creating database");
92              
93 100         844 my $dbh = DBI->connect( $self->storage_dsn );
94 100         30115 $dbh->{RaiseError} = 1;
95             # $dbh->{AutoCommit} = 1;
96              
97 100         605 $self->_set_dbh( $dbh );
98              
99             ###################################################################
100             # Register Extension functions
101              
102 100         551 $self->_log->debug("Register extension functions");
103              
104             $self->_dbh->sqlite_create_function( '_vertex_matches', 2, sub {
105 4200     4200   31601 return !! $self->vertex_matches->(@_);
106 100         1382 });
107              
108             $self->_dbh->sqlite_create_function( '_vertex_nullable', 1, sub {
109 700     700   9786 return !! $self->vertex_nullable->(@_);
110 100         1006 });
111              
112             $self->_dbh->sqlite_create_function( '_canonical', 1, sub {
113             # Since SQLite's json_group_array does not guarantee ordering,
114             # we sort the items in the list ourselves here.
115 4024     4024   181885 my @vertices = $self->_vertex_str_to_vertices(@_);
116 4024         8492 return $self->_vertex_str_from_vertices(@vertices);
117 100         844 });
118              
119             ###################################################################
120             # Deploy schema
121              
122 100         432 $self->_log->debug("Deploying schema");
123 100         582 $self->_deploy_schema();
124              
125             ###################################################################
126             # Insert input data
127              
128 100         179348 $self->_log->debug("Initialising input");
129 100         806 $self->_init_input;
130              
131 100         633 $self->_log->debug("Initialising vertices");
132 100         630 $self->_init_vertices;
133              
134 100         587 $self->_log->debug("Initialising edges");
135 100         606 $self->_init_edges;
136              
137             ###################################################################
138             # Insert pre-computed data
139              
140 100         648 $self->_log->debug("Initialising match data");
141 100         594 $self->_init_matches;
142              
143 100         1478 $self->_log->debug("Computing epsilon closures");
144 100         591 $self->_init_epsilon_closure;
145              
146             ###################################################################
147             # Let DB analyze data so far
148              
149 100         38436 $self->_log->debug("Updating DB statistics");
150 100         945 $self->_dbh->do('ANALYZE');
151              
152             # FIXME: strictly speaking, the dead state is a ombination of all
153             # vertices from which an accepting combination of vertices cannot
154             # be reached. That might be important. Perhaps when later merging
155             # dead states, this would be resolved automatically? Probably not.
156              
157 100         16710 my $dead_state_id = $self->find_or_create_state_id();
158 100         3175 $self->_set_dead_state_id($dead_state_id);
159             }
160              
161             sub _deploy_schema {
162 100     100   250 my ($self) = @_;
163            
164 100         747 local $self->_dbh->{sqlite_allow_multiple_statements} = 1;
165              
166 100         1734 $self->_dbh->do(q{
167             -----------------------------------------------------------------
168             -- Pragmata
169             -----------------------------------------------------------------
170              
171             PRAGMA foreign_keys = ON;
172             PRAGMA synchronous = OFF;
173             PRAGMA journal_mode = OFF;
174             PRAGMA locking_mode = EXCLUSIVE;
175            
176             -----------------------------------------------------------------
177             -- Input Alphabet
178             -----------------------------------------------------------------
179              
180             CREATE TABLE Input (
181             value INTEGER PRIMARY KEY NOT NULL
182             );
183              
184             -----------------------------------------------------------------
185             -- Input Graph Vertex
186             -----------------------------------------------------------------
187              
188             CREATE TABLE Vertex (
189             value INTEGER PRIMARY KEY
190             CHECK(printf("%u", value) = value),
191             is_nullable BOOL
192             );
193              
194             CREATE TRIGGER trigger_Vertex_insert
195             AFTER INSERT ON Vertex
196             BEGIN
197              
198             UPDATE Vertex
199             SET is_nullable = _vertex_nullable(NEW.value)
200             WHERE value = NEW.value;
201              
202             END;
203              
204             -----------------------------------------------------------------
205             -- Input Graph Edges
206             -----------------------------------------------------------------
207              
208             CREATE TABLE Edge (
209             src INTEGER NOT NULL,
210             dst INTEGER NOT NULL,
211             UNIQUE(src, dst),
212             FOREIGN KEY (dst)
213             REFERENCES Vertex(value)
214             ON DELETE NO ACTION
215             ON UPDATE NO ACTION,
216             FOREIGN KEY (src)
217             REFERENCES Vertex(value)
218             ON DELETE NO ACTION
219             ON UPDATE NO ACTION
220             );
221              
222             CREATE INDEX Edge_idx_dst ON Edge (dst);
223              
224             -- can use covering index instead
225             -- CREATE INDEX Edge_idx_src ON Edge (src);
226              
227             CREATE TRIGGER trigger_Edge_insert
228             BEFORE INSERT ON Edge
229             BEGIN
230             INSERT OR IGNORE
231             INTO Vertex(value)
232             VALUES(NEW.src);
233              
234             INSERT OR IGNORE
235             INTO Vertex(value)
236             VALUES(NEW.dst);
237             END;
238              
239             -----------------------------------------------------------------
240             -- Epsilon Closure
241             -----------------------------------------------------------------
242              
243             CREATE TABLE Closure (
244             root INTEGER NOT NULL,
245             e_reachable INTEGER NOT NULL,
246             UNIQUE(root, e_reachable),
247             FOREIGN KEY (root)
248             REFERENCES Vertex(value)
249             ON DELETE NO ACTION
250             ON UPDATE NO ACTION,
251             FOREIGN KEY (e_reachable)
252             REFERENCES Vertex(value)
253             ON DELETE NO ACTION
254             ON UPDATE NO ACTION
255             );
256              
257             CREATE INDEX Closure_idx_dst ON Closure(e_reachable);
258              
259             -- can use covering index instead
260             -- CREATE INDEX Closure_idx_src ON Closure(root);
261              
262             -----------------------------------------------------------------
263             -- DFA States
264             -----------------------------------------------------------------
265              
266             CREATE TABLE State (
267             state_id INTEGER PRIMARY KEY NOT NULL,
268             vertex_str TEXT UNIQUE NOT NULL
269             );
270              
271             -----------------------------------------------------------------
272             -- DFA State Composition
273             -----------------------------------------------------------------
274              
275             CREATE VIEW Configuration AS
276             SELECT
277             State.state_id AS state,
278             each.value AS vertex
279             FROM
280             State
281             INNER JOIN json_each(State.vertex_str) each;
282              
283             CREATE TRIGGER
284             trigger_Configuration_delete
285             INSTEAD OF DELETE ON
286             Configuration
287             FOR EACH ROW BEGIN
288             UPDATE
289             State
290             SET
291             vertex_str = _canonical((
292             SELECT
293             json_group_array(c.vertex)
294             FROM
295             Configuration c
296             WHERE
297             c.vertex <> OLD.vertex
298             GROUP BY
299             c.state
300             ))
301             WHERE
302             State.state_id = OLD.state;
303             END;
304              
305             -----------------------------------------------------------------
306             -- Input Graph Vertex Match data
307             -----------------------------------------------------------------
308              
309             CREATE TABLE Match (
310             vertex INTEGER NOT NULL,
311             input INTEGER NOT NULL,
312             UNIQUE(vertex, input),
313             FOREIGN KEY (input)
314             REFERENCES Input(value)
315             ON DELETE NO ACTION
316             ON UPDATE NO ACTION,
317             FOREIGN KEY (vertex)
318             REFERENCES Vertex(value)
319             ON DELETE NO ACTION
320             ON UPDATE NO ACTION
321             );
322              
323             CREATE INDEX Match_idx_input ON Match (input);
324              
325             -- can use covering index instead
326             -- CREATE INDEX Match_idx_vertex ON Match (vertex);
327              
328             -----------------------------------------------------------------
329             -- DFA Transitions
330             -----------------------------------------------------------------
331              
332             CREATE TABLE Transition (
333             src INTEGER NOT NULL,
334             input INTEGER NOT NULL,
335             dst INTEGER NOT NULL,
336             UNIQUE(src, input),
337             FOREIGN KEY (dst)
338             REFERENCES State(state_id)
339             ON DELETE CASCADE
340             ON UPDATE NO ACTION,
341             FOREIGN KEY (input)
342             REFERENCES Input(value)
343             ON DELETE NO ACTION
344             ON UPDATE NO ACTION,
345             FOREIGN KEY (src)
346             REFERENCES State(state_id)
347             ON DELETE CASCADE
348             ON UPDATE NO ACTION
349             );
350              
351             CREATE INDEX Transition_idx_dst ON Transition (dst);
352             CREATE INDEX Transition_idx_input ON Transition (input);
353              
354             -- can use covering index instead
355             -- CREATE INDEX Transition_idx_src ON Transition (src);
356              
357             -----------------------------------------------------------------
358             -- Views
359             -----------------------------------------------------------------
360              
361             CREATE VIEW view_transitions_as_5tuples AS
362             ---------------------------------------------------------------
363             -- epsilon transitions
364             ---------------------------------------------------------------
365             SELECT
366             s.state_id AS src_state,
367             e.src AS src_vertex,
368             NULL AS via,
369             s.state_id AS dst_state,
370             e.dst AS dst_vertex
371             FROM
372             State s
373             INNER JOIN Configuration c1 ON (c1.state = s.state_id)
374             INNER JOIN Configuration c2 ON (c2.state = s.state_id)
375             INNER JOIN Edge e
376             ON (e.src = c1.vertex AND e.dst = c2.vertex)
377             INNER JOIN Vertex v
378             ON (v.value = e.src AND v.is_nullable = 1)
379              
380             UNION ALL
381              
382             ---------------------------------------------------------------
383             -- transitions over terminals
384             ---------------------------------------------------------------
385             SELECT
386             tr.src AS src_state,
387             e.src AS src_vertex,
388             tr.input AS via,
389             tr.dst AS dst_state,
390             e.dst AS dst_vertex
391             FROM
392             Transition tr
393             INNER JOIN Configuration c1 ON (c1.state = tr.src)
394             INNER JOIN Configuration c2 ON (c2.state = tr.dst)
395             INNER JOIN Edge e
396             ON (e.src = c1.vertex AND e.dst = c2.vertex)
397             INNER JOIN Match m
398             ON (m.input = tr.input AND m.vertex = c1.vertex);
399            
400             CREATE VIEW view_transitions_as_configuration_pair AS
401             SELECT
402             c1.rowid AS src_id,
403             c2.rowid AS dst_id
404             FROM
405             view_transitions_as_5tuples t
406             INNER JOIN Configuration c1
407             ON (c1.state = t.src_state
408             AND c1.vertex = t.src_vertex)
409             INNER JOIN Configuration c2
410             ON (c2.state = t.dst_state
411             AND c2.vertex = t.dst_vertex);
412             });
413             }
414              
415             sub _insert_or_ignore {
416 300     300   937 my ($self, $table, $values, @cols) = @_;
417              
418             my $cols_str = join ", ",
419 300         764 map { $self->_dbh->quote_identifier($_) } @cols;
  400         4169  
420              
421             my $placeholders_str = join ", ",
422 300         12246 map { '?' } @cols;
  400         857  
423              
424 300         1193 my $table_str = $self->_dbh->quote_identifier($table);
425              
426 300         8140 my $sth = $self->_dbh->prepare(sprintf q{
427             INSERT OR IGNORE INTO %s(%s) VALUES (%s)
428             }, $table_str, $cols_str, $placeholders_str);
429              
430 300         27447 $self->_dbh->begin_work();
431 300 100       34332 $sth->execute(ref($_) eq 'ARRAY' ? @$_ : $_) for @$values;
432 300         6503 $self->_dbh->commit();
433             }
434              
435             sub _init_input {
436 100     100   354 my ($self) = @_;
437 100         641 _insert_or_ignore($self, 'Input', $self->input_alphabet, 'value');
438             }
439              
440             sub _init_vertices {
441 100     100   300 my ($self) = @_;
442 100         489 _insert_or_ignore($self, 'Vertex', $self->input_vertices, 'value');
443             }
444              
445             sub _init_edges {
446 100     100   241 my ($self) = @_;
447 100         383 _insert_or_ignore($self, 'Edge', $self->input_edges, 'src', 'dst');
448             }
449              
450             sub _init_matches {
451 100     100   357 my ($self) = @_;
452              
453 100         508 $self->_dbh->do(q{
454             INSERT INTO Match(vertex, input)
455             SELECT Vertex.value, Input.value
456             FROM
457             Vertex CROSS JOIN Input
458             WHERE
459             _vertex_matches(Vertex.value, Input.value)+0 = 1
460             ORDER BY Vertex.value, Input.value
461             });
462             }
463              
464             sub _init_epsilon_closure {
465 100     100   228 my ($self) = @_;
466              
467 100         552 $self->_dbh->do(q{
468             INSERT INTO Closure(root, e_reachable)
469             WITH RECURSIVE all_e_successors_and_self(root, v) AS (
470              
471             SELECT value AS root, value AS v FROM vertex
472              
473             UNION
474              
475             SELECT r.root, Edge.dst
476             FROM Edge
477             INNER JOIN all_e_successors_and_self AS r
478             ON (Edge.src = r.v)
479             INNER JOIN Vertex AS src_vertex
480             ON (Edge.src = src_vertex.value)
481             WHERE src_vertex.is_nullable
482             )
483             SELECT root, v FROM all_e_successors_and_self
484             ORDER BY root, v
485             });
486             }
487              
488             sub _vertex_str_from_vertices {
489 4124     4124   7237 my ($self, @vertices) = @_;
490              
491             return $self->_json->encode([
492 4124     11010   14879 nsort_by { $_ } uniq(grep { defined } @vertices)
  11010         47047  
  28706         57053  
493             ]);
494             }
495              
496             sub _vertex_str_to_vertices {
497 4678     4678   8951 my ($self, $vertex_str) = @_;
498              
499 4678         6520 return @{ $self->_json->decode($vertex_str) };
  4678         28178  
500             }
501              
502             sub _find_state_id_by_vertex_str {
503 1636     1636   2749 my ($self, $vertex_str) = @_;
504              
505 1636         8695 my $sth = $self->_dbh->prepare(q{
506             SELECT state_id FROM State WHERE vertex_str = ?
507             });
508              
509 1636         120352 return $self->_dbh->selectrow_array($sth, {}, $vertex_str);
510             }
511              
512             sub _find_or_create_state_from_vertex_str {
513 1636     1636   3213 my ($self, $vertex_str) = @_;
514              
515 1636         3079 my $state_id = _find_state_id_by_vertex_str($self, $vertex_str);
516              
517 1636 100       7933 return $state_id if defined $state_id;
518              
519 654         2977 $self->_dbh->begin_work();
520              
521 654         10178 my $sth = $self->_dbh->prepare(q{
522             INSERT INTO State(vertex_str) VALUES (?)
523             });
524              
525 654         39714 $sth->execute($vertex_str);
526              
527 654         3426 $state_id = $self->_dbh->sqlite_last_insert_rowid();
528              
529 654         4455 $self->_dbh->commit();
530 654         7096 return $state_id;
531             }
532              
533             sub _vertex_str_from_partial_list {
534 200     200   502 my ($self, @vertices) = @_;
535              
536 200 100       837 return $self->_vertex_str_from_vertices() unless @vertices;
537              
538             my $escaped_roots = join ", ", map {
539 100         302 $self->_dbh->quote($_)
  100         780  
540             } @vertices;
541              
542 100         2334 my ($vertex_str) = $self->_dbh->selectrow_array(qq{
543             SELECT _canonical(json_group_array(closure.e_reachable))
544             FROM Closure
545             WHERE root IN ($escaped_roots)
546             });
547              
548 100         3232 return $vertex_str;
549             }
550              
551             sub find_or_create_state_id {
552 200     200 1 5110 my ($self, @vertices) = @_;
553              
554 200         557 my $vertex_str = _vertex_str_from_partial_list($self, @vertices);
555              
556 200         2672 return _find_or_create_state_from_vertex_str($self, $vertex_str);
557             }
558              
559             sub vertices_in_state {
560 14964     14964 1 6513829 my ($self, $state_id) = @_;
561              
562 14964         70813 return map { @$_ } $self->_dbh->selectall_array(q{
  76346         1975450  
563             SELECT vertex FROM Configuration WHERE state = ?
564             }, {}, $state_id);
565             }
566              
567             sub cleanup_dead_states {
568 100     100 1 30997 my ($self, $vertices_accept) = @_;
569              
570             $self->_dbh->sqlite_create_function( '_vertices_accept', 1, sub {
571 654     654   18321 my @vertices = $self->_vertex_str_to_vertices(@_);
572 654         1472 return !! $vertices_accept->(@vertices);
573 100         1469 });
574              
575 100         543 $self->_dbh->begin_work();
576              
577 100         1866 $self->_dbh->do(q{
578             CREATE TEMPORARY TABLE accepting AS
579             SELECT state_id AS state
580             FROM State
581             WHERE _vertices_accept(vertex_str)+0 = 1
582             });
583              
584 100         7207 my @accepting = map { @$_ } $self->_dbh->selectall_array(q{
  363         8692  
585             SELECT state FROM accepting
586             });
587              
588             # NOTE: this also renames states in transitions involving
589             # possible start states, but they would then simply have no
590             # transitions, which should be fine.
591              
592 100         883 $self->_dbh->do(q{
593             WITH RECURSIVE all_living(state) AS (
594             SELECT state FROM accepting
595            
596             UNION
597            
598             SELECT src AS state
599             FROM Transition
600             INNER JOIN all_living
601             ON (Transition.dst = all_living.state)
602             )
603             UPDATE Transition
604             SET dst = ?
605             WHERE dst NOT IN (SELECT state FROM all_living)
606             }, {}, $self->dead_state_id);
607              
608 100         50120 $self->_dbh->do(q{
609             DROP TABLE accepting;
610             });
611              
612 100         14708 $self->_dbh->commit();
613              
614             # TODO: is there a better way to drop the function?
615 100         825 $self->_dbh->sqlite_create_function( '_vertices_accept', 1, undef );
616              
617 100         448 return @accepting;
618             }
619              
620             sub compute_some_transitions {
621 471     471 1 125819 my ($self, $limit) = @_;
622              
623 471   50     1412 $limit //= 1_000;
624              
625 471         3205 my $sth = $self->_dbh->prepare_cached(q{
626             SELECT
627             s.state_id AS src
628             , i.value AS input
629             , _canonical(json_group_array(closure.e_reachable))
630             AS dst_vertex_str
631             FROM
632             state s
633             CROSS JOIN input i
634             LEFT JOIN configuration c
635             ON (s.state_id = c.state)
636             LEFT JOIN match m
637             ON (m.vertex = c.vertex AND m.input = i.value)
638             LEFT JOIN edge
639             ON (m.vertex = edge.src)
640             LEFT JOIN closure
641             ON (edge.dst = closure.root)
642             LEFT JOIN transition t
643             ON (t.src = s.state_id AND t.input = i.value)
644             WHERE
645             t.dst IS NULL
646             GROUP BY
647             s.state_id, i.rowid
648             ORDER BY
649             s.state_id, i.rowid
650             LIMIT ?
651             });
652              
653 471         32215 my @new = $self->_dbh->selectall_array($sth, {}, $limit);
654              
655             my $find_or_create = memoize(sub {
656 1436     1436   37915 _find_or_create_state_from_vertex_str($self, @_);
657 471         52567 });
658              
659 471         90842 my $sth2 = $self->_dbh->prepare(q{
660             INSERT INTO Transition(src, input, dst) VALUES (?, ?, ?)
661             });
662              
663 471         29930 my @transitions;
664              
665 471         1106 for my $t (@new) {
666 3924         121013 push @transitions, [(
667             $t->[0],
668             $t->[1],
669             $find_or_create->($t->[2]),
670             )];
671             }
672              
673 471         9235 $self->_dbh->begin_work();
674 471         40182 $sth2->execute(@$_) for @transitions;
675 471         3756 $self->_dbh->commit();
676              
677 471         6766 return scalar @new;
678             }
679              
680             sub transitions_as_3tuples {
681 100     100 1 16479398 my ($self) = @_;
682              
683 100         796 return $self->_dbh->selectall_array(q{
684             SELECT src, input, dst FROM transition
685             });
686             }
687              
688             sub transitions_as_5tuples {
689 0     0 1   my ($self) = @_;
690              
691 0           return $self->_dbh->selectall_array(q{
692             SELECT * FROM view_transitions_as_5tuples
693             });
694             }
695              
696             sub backup_to_file {
697 0     0 1   my ($self, $schema_version, $file) = @_;
698 0 0         die unless $schema_version eq 'v0';
699 0           $self->_dbh->sqlite_backup_to_file($file);
700             }
701              
702             # sub backup_to_dbh {
703             # my ($self, $schema_version) = @_;
704             #
705             # die unless $schema_version eq 'v0';
706             #
707             # require File::Temp;
708             #
709             # my ($fh, $filename) = File::Temp::tempfile();
710             #
711             # $self->_dbh->sqlite_backup_to_file($filename);
712             #
713             # my $dbh = DBI->connect('dbi:SQLite:dbname=:memory:');
714             #
715             # $dbh->sqlite_backup_from_file($filename);
716             #
717             # File::Temp::unlink0($fh, $filename);
718             #
719             # undef $fh;
720             #
721             # return $dbh;
722             # }
723              
724             1;
725              
726             __END__