File Coverage

blib/lib/Dallycot/Library.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             package Dallycot::Library;
2             our $AUTHORITY = 'cpan:JSMITH';
3              
4             # ABSTRACT: Base for adding namespaced functions to Dallycot.
5              
6             =head1 SYNOPSIS
7              
8             package MyLibrary;
9              
10             use Moose;
11             extends 'Dallycot::Library';
12              
13             ns 'http://www.example.com/library#';
14              
15             define foo => << 'EOD';
16             (a, b) :> ((a * b) mod (b - a))
17             EOD
18              
19             define bar => sub {
20             my($library, $engine, $options, @params) = @_;
21             # Perl implementation
22             };
23              
24             =cut
25              
26 1     1   4 use strict;
  1         2  
  1         29  
27 1     1   4 use warnings;
  1         1  
  1         20  
28              
29 1     1   3 use utf8;
  1         2  
  1         4  
30 1     1   15 use MooseX::Singleton;
  1         1  
  1         5  
31              
32 1     1   8341 use namespace::autoclean -except => [qw/_libraries/];
  1         2  
  1         8  
33              
34 1     1   81 use MooseX::Types::Moose qw/ArrayRef CodeRef/;
  1         3  
  1         10  
35 1     1   3793 use Carp qw(croak);
  1         3  
  1         55  
36              
37 1     1   5 use Dallycot::Parser;
  1         1  
  1         61  
38 1     1   22 use Dallycot::Processor;
  0            
  0            
39              
40             use AnyEvent;
41              
42             use Moose::Exporter;
43              
44             use Promises qw(deferred collect);
45              
46             use Module::Pluggable
47             inner => 1,
48             instantiate => 'instance',
49             sub_name => '_libraries',
50             search_path => 'Dallycot::Library';
51              
52             our @LIBRARIES;
53              
54             sub libraries {
55             return @LIBRARIES if @LIBRARIES;
56             return @LIBRARIES = grep { $_->isa('Dallycot::Library') } shift->_libraries;
57             }
58              
59             my %engines;
60              
61             my %namespaces;
62              
63             sub ns {
64             my ( $meta, $uri ) = @_;
65              
66             Dallycot::Registry->instance->register_namespace( $uri, $meta->{'package'} );
67              
68             $namespaces{ $meta->{'package'} } = $uri;
69              
70             my $engine = $engines{ $meta->{'package'} } ||= Dallycot::Processor->new;
71             uses( $meta, $uri );
72             return;
73             }
74              
75             sub namespace {
76             my ($class) = @_;
77              
78             $class = ref($class) || $class;
79              
80             return $namespaces{$class};
81             }
82              
83             my %definitions;
84             my %uses_promises;
85              
86             sub define {
87             my ( $meta, $name, @options ) = @_;
88              
89             my $body = pop @options;
90             my %options = @options;
91              
92             my $definitions = $definitions{ $meta->{'package'} } ||= {};
93              
94             if ( is_CodeRef($body) ) {
95              
96             # Perl subroutine
97             my $uri_promise = deferred;
98             $uri_promise->resolve( $meta->{'package'}->_uri_for_name($name) );
99              
100             $definitions->{$name} = {
101             %options,
102             uri => $uri_promise,
103             coderef => $body
104             };
105             }
106             else {
107             # Dallycot source
108             my $parser = Dallycot::Parser->new;
109             my $parsed = $parser->parse($body);
110             my $engine = $engines{ $meta->{'package'} } ||= Dallycot::Processor->new;
111              
112             if ( !$parsed ) {
113             croak "Unable to parse Dallycot source for $name";
114             }
115              
116             $uses_promises{ $meta->{'package'} }->done(
117             sub {
118             $definitions->{$name} = {
119             %options,
120             expression => $engine->with_child_scope->execute( @{$parsed} )->catch(
121             sub {
122             my ($err) = @_;
123              
124             print STDERR "Error defining $name: $err\n";
125             croak $err;
126             }
127             )
128             };
129             }
130             );
131             }
132             return;
133             }
134              
135             sub uses {
136             my ( $meta, @uris ) = @_;
137              
138             my $engine = $engines{ $meta->{'package'} } ||= Dallycot::Processor->new;
139              
140             my $promise = Dallycot::Registry->instance->register_used_namespaces(@uris)->then(
141             sub {
142             $engine->append_namespace_search_path(@uris);
143             }
144             );
145              
146             my $prior_promise = $uses_promises{ $meta->{'package'} };
147             if ($prior_promise) {
148             $prior_promise = $prior_promise->then( sub {$promise} );
149             }
150             else {
151             $prior_promise = $promise;
152             }
153             $uses_promises{ $meta->{'package'} } = $prior_promise;
154              
155             return;
156             }
157              
158             Moose::Exporter->setup_import_methods(
159             with_meta => [qw(ns define uses)],
160             also => 'Moose',
161             );
162              
163             sub init_meta {
164             my ( undef, %p ) = @_;
165              
166             my $meta = MooseX::Singleton->init_meta(%p);
167             $meta->superclasses(__PACKAGE__);
168             return $meta;
169             }
170              
171             sub has_assignment {
172             my ( $self, $name ) = @_;
173              
174             my $def = $self->get_definition($name);
175             return defined($def) && keys %$def;
176             }
177              
178             sub get_assignment {
179             my ( $self, $name ) = @_;
180              
181             my $class = ref($self) || $self;
182              
183             my $def = $self->get_definition($name);
184              
185             return unless defined $def && keys %$def;
186             if ( $def->{expression} ) {
187             return $def->{expression};
188             }
189             else {
190             return $def->{uri};
191             }
192             }
193              
194             sub _uri_for_name {
195             my ( $class, $name ) = @_;
196              
197             $class = ref($class) || $class;
198              
199             return Dallycot::Value::URI->new( $class->namespace . $name );
200             }
201              
202             sub get_definition {
203             my ( $class, $name ) = @_;
204              
205             return unless defined $name;
206              
207             $class = ref($class) || $class;
208              
209             my $definitions = $definitions{$class};
210              
211             if ( exists $definitions->{$name} && defined $definitions->{$name} ) {
212             return $definitions->{$name};
213             }
214             else {
215             return;
216             }
217             }
218              
219             sub get_definitions {
220             my ( $class ) = @_;
221              
222             $class = ref($class) || $class;
223              
224             return keys %{$definitions{$class} || {}};
225             }
226              
227             sub min_arity {
228             my ( $self, $name ) = @_;
229              
230             my $def = $self->get_definition($name);
231              
232             if ( !$def ) {
233             return 0;
234             }
235              
236             if ( $def->{coderef} ) {
237             if ( defined( $def->{arity} ) ) {
238             if ( is_ArrayRef( $def->{arity} ) ) {
239             return $def->{arity}->[0];
240             }
241             else {
242             return $def->{arity};
243             }
244             }
245             else {
246             return 0;
247             }
248             }
249             else {
250             return 0;
251             }
252             }
253              
254             sub _is_placeholder {
255             my ( $self, $obj ) = @_;
256              
257             return blessed($obj) && $obj->isa('Dallycot::AST::Placeholder');
258             }
259              
260             sub apply {
261             my ( $self, $name, $parent_engine, $options, @bindings ) = @_;
262              
263             my $def = $self->get_definition($name);
264              
265             if ( !$def ) {
266             my $d = deferred;
267             $d->reject("$name is undefined.");
268             return $d->promise;
269             }
270              
271             if ( $def->{coderef} ) {
272             if ( defined $def->{arity} ) {
273             if ( is_ArrayRef( $def->{arity} ) ) {
274             if ( $def->{arity}->[0] > @bindings
275             || ( @{ $def->{arity} } > 1 && @bindings > $def->{arity}->[1] ) )
276             {
277             my $d = deferred;
278             $d->reject( "Expected "
279             . $def->{arity}->[0] . " to "
280             . $def->{arity}->[1]
281             . " arguments but found "
282             . scalar(@bindings) );
283             return $d->promise;
284             }
285             }
286             elsif ( $def->{arity} != @bindings ) {
287             my $d = deferred;
288             $d->reject( "Expected " . $def->{arity} . " argument(s)s but found " . scalar(@bindings) );
289             return $d->promise;
290             }
291             }
292              
293             # we look for placeholders and return a lambda if there are any
294             if ( grep { $self->_is_placeholder($_) } @bindings ) {
295             my ( @filled_bindings, @filled_identifiers, @args, @new_args );
296             foreach my $binding (@bindings) {
297             if ( $self->_is_placeholder($binding) ) {
298             push @new_args, '__arg_' . $#args;
299             push @args, '__arg_' . $#args;
300             }
301             else {
302             push @filled_identifiers, '__arg_' . $#args;
303             push @args, '__arg_' . $#args;
304             push @filled_bindings, $binding;
305             }
306             }
307             my $engine = $parent_engine->with_child_scope;
308             return collect( $engine->collect(@filled_bindings), $engine->collect( values %$options ) )->then(
309             sub {
310             my ( $collected_bindings, $new_values ) = @_;
311             my @collected_bindings = @$collected_bindings;
312             my @new_values = @$new_values;
313             my %new_options;
314             @new_options{ keys %$options } = @new_values;
315             return Dallycot::Value::Lambda->new(
316             expression => Dallycot::AST::Apply->new(
317             $self->_uri_for_name($name),
318             [ map { bless [$_] => 'Dallycot::AST::Fetch' } @args ],
319             ),
320             bindings => \@new_args,
321             options => \%new_options,
322             closure_environment =>
323             { map { $filled_identifiers[$_] => $collected_bindings[$_] } ( 0 .. $#filled_identifiers ) }
324             );
325             }
326             );
327             }
328             elsif ( $def->{hold} ) {
329             my $engine = $parent_engine->with_child_scope;
330             return $def->{coderef}->( $engine, $options, @bindings );
331             }
332             else {
333             my $engine = $parent_engine->with_child_scope;
334             return collect( $engine->collect(@bindings), $engine->collect( values %$options ) )->then(
335             sub {
336             my ( $collected_bindings, $new_values ) = @_;
337              
338             my @collected_bindings = @$collected_bindings;
339             my @new_values = @$new_values;
340             my %new_options;
341             @new_options{ keys %{ $def->{options} || {} } } = values %{ $def->{options} || {} };
342             @new_options{ keys %$options } = @new_values;
343             $def->{coderef}->( $engine, \%new_options, @collected_bindings );
344             }
345             );
346             }
347             }
348             elsif ( $def->{expression} ) {
349             my $engine = $parent_engine->with_child_scope;
350             return $def->{expression}->then(
351             sub {
352             my ($lambda) = @_;
353             $lambda->apply( $engine, $options, @bindings );
354             }
355             );
356             }
357             else {
358             my $d = deferred;
359             $d->reject("Value is not a lambda");
360             return $d->promise;
361             }
362             }
363              
364             __PACKAGE__->meta->make_immutable;
365              
366             __PACKAGE__->libraries;
367              
368             1;