use v5.14;
use warnings;
use utf8;

=head1 NAME

Attean::Plan - Representation of SPARQL query plan operators

=head1 VERSION

This document describes Attean::Plan version 0.037

=head1 SYNOPSIS

  use v5.14;
  use Attean;

=head1 DESCRIPTION

This is a utility package that defines all the Attean query plan classes
in the Attean::Plan namespace:

=over 4

=cut

use Attean::API::Query;

=item * L<Attean::Plan::Quad>

Evaluates a quad pattern against the model.

=cut

package Attean::Plan::Quad 0.037 {
	use Moo;
	use Scalar::Util qw(blessed reftype);
	use Types::Standard qw(ConsumerOf ArrayRef);
	use AtteanX::Functions::CompositeLists;
	use AtteanX::Functions::CompositeMaps;
	use namespace::clean;

	has 'subject'	=> (is => 'ro', required => 1);
	has 'predicate'	=> (is => 'ro', required => 1);
	has 'object'	=> (is => 'ro', required => 1);
	has 'graph'		=> (is => 'ro', required => 1);
	
	with 'Attean::API::BindingSubstitutionPlan', 'Attean::API::NullaryQueryTree';
	with 'Attean::API::QuadPattern';

	around 'BUILDARGS' => sub {
		my $orig		= shift;
		my $class		= shift;
		my $args		= $orig->( $class, @_ );
		if (exists $args->{in_scope_variables}) {
			Carp::confess "in_scope_variables is computed automatically, and must not be specified in the $class constructor";
		}
		
		my %vars;
		foreach my $pos (qw(subject predicate object graph)) {
			my $term	= $args->{$pos};
			if (blessed($term) and $term->does('Attean::API::Variable')) {
				$vars{$term->value}	= $term;
			}
		}
		
		my @vars	= keys %vars;
		$args->{in_scope_variables}	= [@vars];

		return $args;
	};

	sub plan_as_string {
		my $self	= shift;
		my @nodes	= $self->values;
		my @strings;
		foreach my $t (@nodes) {
			if (ref($t) eq 'ARRAY') {
				my @tstrings	= map { $_->ntriples_string } @$t;
				if (scalar(@tstrings) == 1) {
					push(@strings, @tstrings);
				} else {
					push(@strings, '[' . join(', ', @tstrings) . ']');
				}
			} elsif ($t->does('Attean::API::TermOrVariable')) {
				push(@strings, $t->ntriples_string);
			} else {
				use Data::Dumper;
				die "Unrecognized node in quad pattern: " . Dumper($t);
			}
		}
		return sprintf('Quad { %s }', join(', ', @strings));
	}
	
	sub substitute_impl {
		my $self	= shift;
		my $model	= shift;
		my $b		= shift;
		my @values	= $self->values;
		foreach my $i (0 .. $#values) {
			my $value	= $values[$i];
			if (reftype($value) eq 'ARRAY') {
				my @values;
				foreach my $value (@{ $value }) {
					my $name	= $value->value;
					if (my $node = $b->value($name)) {
						push(@values, $node);
					} else {
						push(@values, $value);
					}
					$values[$i]	= \@values;
				}
			} elsif ($value->does('Attean::API::Variable')) {
				my $name	= $value->value;
				if (my $node = $b->value($name)) {
					$values[$i]	= $node;
				}
			}
		}
		
		return sub {
			return $model->get_bindings( @values );
		}
	}

	sub impl {
		my $self	= shift;
		my $model	= shift;
		my @values	= $self->values;
		return sub {
			return $model->get_bindings( @values );
		}
	}
}

=item * L<Attean::Plan::NestedLoopJoin>

Evaluates a join (natural-, anti-, or left-) using a nested loop.

=cut

package Attean::Plan::NestedLoopJoin 0.037 {
	use Moo;
	use List::Util qw(all);
	use namespace::clean;

	with 'Attean::API::BindingSubstitutionPlan';
	with 'Attean::API::Plan::Join';
	sub plan_as_string {
		my $self	= shift;
		if ($self->left) {
			return 'NestedLoop Left Join';
		} elsif ($self->anti) {
			return 'NestedLoop Anti Join';
		} else {
			return 'NestedLoop Join';
		}
	}

	sub impl {
		my $self	= shift;
		my $model	= shift;
		my @children	= map { $_->impl($model) } @{ $self->children };
		return $self->_impl($model, @children);
	}
	
	sub substitute_impl {
		my $self	= shift;
		my $model	= shift;
		my $b		= shift;
		unless (all { $_->does('Attean::API::BindingSubstitutionPlan') } @{ $self->children }) {
			die "Plan children do not all consume BindingSubstitutionPlan role:\n" . $self->as_string;
		}
		
		my @children	= map { $_->substitute_impl($model, $b) } @{ $self->children };
		return $self->_impl($model, @children);
	}
	
	sub _impl {
		my $self		= shift;
		my $model		= shift;
		my @children	= @_;
		my $left		= $self->left;
		my $anti		= $self->anti;
		my $iter_variables	= $self->in_scope_variables;
		
		return sub {
			my ($lhs, $rhs)	= map { $_->() } @children;
			my @right	= $rhs->elements;
			my @results;
			while (my $l = $lhs->next) {
				my $seen	= 0;
				foreach my $r (@right) {
					my @shared	= $l->shared_domain($r);
					if ($anti and scalar(@shared) == 0) {
						# in a MINUS, two results that have disjoint domains are considered not to be joinable
						next;
					}
					if (my $j = $l->join($r)) {
						$seen++;
						if ($left) {
							# TODO: filter with expression
							push(@results, $j);
						} elsif ($anti) {
						} else {
							push(@results, $j);
						}
					}
				}
				if ($left and not($seen)) {
					push(@results, $l);
				} elsif ($anti and not($seen)) {
					push(@results, $l);
				}
			}
			return Attean::ListIterator->new(
				item_type => 'Attean::API::Result',
				variables => $iter_variables,
				values => \@results,
			);
		}
	}
}

=item * L<Attean::Plan::HashJoin>

Evaluates a join (natural-, anti-, or left-) using a hash join.

=cut

package Attean::Plan::HashJoin 0.037 {
	use Moo;
	use List::Util qw(all);
	use namespace::clean;
	
	sub BUILD {
		my $self	= shift;
		if ($self->anti) {
			die "Cannot use a HashJoin for anti-joins";
		}
	}
	
	with 'Attean::API::BindingSubstitutionPlan';
	with 'Attean::API::Plan::Join';
	sub plan_as_string {
		my $self	= shift;
		my $name;
		if ($self->left) {
			$name	= "Hash Left Join";
		} else {
			$name	= "Hash Join";
		}
		return sprintf('%s { %s }', $name, join(', ', @{$self->join_variables}));
	}

	sub impl {
		my $self	= shift;
		my $model	= shift;
		my @children	= map { $_->impl($model) } @{ $self->children };
		return $self->_impl($model, @children);
	}
	
	sub substitute_impl {
		my $self	= shift;
		my $model	= shift;
		my $b		= shift;
		
		unless (all { $_->does('Attean::API::BindingSubstitutionPlan') } @{ $self->children }) {
			die "Plan children do not all consume BindingSubstitutionPlan role:\n" . $self->as_string;
		}
		
		my @children	= map { $_->substitute_impl($model, $b) } @{ $self->children };
		return $self->_impl($model, @children);
	}
	
	sub _impl {
		my $self		= shift;
		my $model		= shift;
		my @children	= @_;
		my $left	= $self->left;
		my $iter_variables	= $self->in_scope_variables;

		return sub {
			my %hash;
			my @vars	= @{ $self->join_variables };
			my $rhs		= $children[1]->();
			while (my $r = $rhs->next()) {
				my $has_unbound_right_join_var	= 0;
				my @values;
				foreach my $var (@vars) {
					my $value	= $r->value($var);
					unless (defined($value)) {
						$has_unbound_right_join_var++;
					}
					push(@values, $value);
				}
				if ($has_unbound_right_join_var) {
					# this is a RHS row that doesn't have a term bound to one of the join variables.
					# this will make it impossible to compute the proper hash key to access the row bucket,
					# so we add this row to the null bucket (hash key '') which we try to join all LHS rows
					# against.
					push(@{ $hash{''} }, $r);
				} else {
					my $key	= join(',', map { ref($_) ? $_->as_string : '' } @values);
					push(@{ $hash{$key} }, $r);
				}
			}
			
			my @results;
			my $lhs		= $children[0]->();
			while (my $l = $lhs->next()) {
				my $seen	= 0;
				my @values;
				my $has_unbound_left_join_var	= 0;
				foreach my $var (@vars) {
					my $value	= $l->value($var);
					unless (defined($value)) {
						$has_unbound_left_join_var++;
					}
					push(@values, $value);
				}
				
				my @buckets;
				if (my $b = $hash{''}) {
					push(@buckets, $b);
				}
				
				if ($has_unbound_left_join_var) {
					my $pattern	= join(',', map { ref($_) ? quotemeta($_->as_string) : '.*' } @values);
					foreach my $key (keys %hash) {
						if ($key =~ /^${pattern}$/) {
							push(@buckets, $hash{$key});
						}
					}
				} else {
					my $key		= join(',', map { ref($_) ? $_->as_string : '' } @values);
					if (my $rows = $hash{$key}) {
						push(@buckets, $rows);
					}
				}
				
				foreach my $rows (@buckets) {
					foreach my $r (@$rows) {
						if (my $j = $l->join($r)) {
							$seen++;
							if ($left) {
								# TODO: filter with expression
								push(@results, $j);
							} else {
								push(@results, $j);
							}
						}
					}
				}
				if ($left and not($seen)) {
					push(@results, $l);
				}
			}
			return Attean::ListIterator->new(
				item_type => 'Attean::API::Result',
				variables => $iter_variables,
				values => \@results
			);
		}
	}
}

=item * L<Attean::Plan::Construct>

=cut

package Attean::Plan::Construct 0.037 {
	use Moo;
	use List::Util qw(all);
	use Types::Standard qw(Str ArrayRef ConsumerOf InstanceOf);
	use namespace::clean;
	has 'triples' => (is => 'ro', 'isa' => ArrayRef[ConsumerOf['Attean::API::TripleOrQuadPattern']], required => 1);

	with 'Attean::API::BindingSubstitutionPlan', 'Attean::API::UnaryQueryTree';

	sub plan_as_string {
		my $self	= shift;
		my $triples	= $self->triples;
		return sprintf('Construct { %s }', join(' . ', map { $_->as_string } @$triples));
	}

	sub BUILDARGS {
		# TODO: this code is repeated in several plan classes; figure out a way to share it.
		my $class		= shift;
		my %args		= @_;
		my %vars		= map { $_ => 1 } map { @{ $_->in_scope_variables } } @{ $args{ children } };
		my @vars		= keys %vars;
		
		if (exists $args{in_scope_variables}) {
			Carp::confess "in_scope_variables is computed automatically, and must not be specified in the $class constructor";
		}
		$args{in_scope_variables}	= \@vars;

		return $class->SUPER::BUILDARGS(%args);
	}

	sub impl {
		my $self	= shift;
		my $model	= shift;
		my @children	= map { $_->impl($model) } @{ $self->children };
		return $self->_impl($model, @children);
	}
	
	sub substitute_impl {
		my $self	= shift;
		my $model	= shift;
		my $b		= shift;
		unless (all { $_->does('Attean::API::BindingSubstitutionPlan') } @{ $self->children }) {
			die "Plan children do not all consume BindingSubstitutionPlan role:\n" . $self->as_string;
		}
		
		warn "TODO: fix substitute_impl to substitute construct triples";
		my @children	= map { $_->substitute_impl($model, $b) } @{ $self->children };
		return $self->_impl($model, @children);
	}
	
	# replace blank nodes in all the triple patterns with fresh ones
	sub refresh_triples {
		my $self	= shift;
		my @t;
		my %mapping;
		foreach my $t (@_) {
			foreach my $term ($t->values) {
				if ($term->does('Attean::API::Blank')) {
					$mapping{$term->as_string}	= Attean::Blank->new();
				}
			}
		}
		my $mapper	= Attean::TermMap->rewrite_map(\%mapping);
		foreach my $t (@_) {
			push(@t, $t->apply_map($mapper));
		}
		return @t;
	}
	
	sub _impl {
		my $self		= shift;
		my $model		= shift;
		my $child		= shift;
		
		my @triples		= @{ $self->triples };
		
		return sub {
			my $iter	= $child->();
			my @buffer;
			my %seen;
			return Attean::CodeIterator->new(
				item_type => 'Attean::API::Triple',
				generator => sub {
					if (scalar(@buffer)) {
						return shift(@buffer);
					}
					while (my $row = $iter->next) {
						foreach my $tp ($self->refresh_triples(@triples)) {
							my $tp	= $tp->apply_bindings($row);
							my $t	= eval { $tp->as_triple };
							if ($t) {
								push(@buffer, $t);
							}
						}
						if (scalar(@buffer)) {
							my $t	= shift(@buffer);
							return $t;
						}
					}
				}
			)->grep(sub {
				return not $seen{$_->as_string}++;
			});
		}
	}
}

=item * L<Attean::Plan::Describe>

=cut

package Attean::Plan::Describe 0.037 {
	use Moo;
	use Attean::RDF;
	use List::Util qw(all);
	use Types::Standard qw(Str ArrayRef ConsumerOf InstanceOf);
	use namespace::clean;

	has 'graph'	=> (is => 'ro');
	has 'terms' => (is => 'ro', isa => ArrayRef[ConsumerOf['Attean::API::TermOrVariable']]);

	with 'Attean::API::BindingSubstitutionPlan', 'Attean::API::UnaryQueryTree';
	with 'Attean::API::UnionScopeVariablesPlan';

	sub plan_as_string {
		my $self	= shift;
		my $terms	= $self->terms;
		return sprintf('Describe { %s }', join(' . ', map { $_->as_string } @$terms));
	}

	sub impl {
		my $self	= shift;
		my $model	= shift;
		my @children	= map { $_->impl($model) } @{ $self->children };
		return $self->_impl($model, @children);
	}
	
	sub substitute_impl {
		my $self	= shift;
		my $model	= shift;
		my $b		= shift;
		unless (all { $_->does('Attean::API::BindingSubstitutionPlan') } @{ $self->children }) {
			die "Plan children do not all consume BindingSubstitutionPlan role:\n" . $self->as_string;
		}
		
		warn "TODO: fix substitute_impl to substitute describe terms";
		my @children	= map { $_->substitute_impl($model, $b) } @{ $self->children };
		return $self->_impl($model, @children);
	}
	
	sub _impl {
		my $self		= shift;
		my $model		= shift;
		my $child		= shift;
		
		my $graph		= $self->graph;
		my @terms		= @{ $self->terms };
		# TODO: Split @terms into ground terms and variables.
		#       Only call get_quads once for ground terms.
		#       For variable terms, call get_quads for each variable-result combination.
		return sub {
			my $iter	= $child->();
			my @buffer;
			my %seen;
			return Attean::CodeIterator->new(
				item_type => 'Attean::API::Triple',
				generator => sub {
					if (scalar(@buffer)) {
						return shift(@buffer);
					}
					while (my $row = $iter->next) {
						foreach my $term (@terms) {
							my $value	= $term->apply_binding($row);
							if ($value->does('Attean::API::Term')) {
								my $iter	= $model->get_quads( $value, variable('predicate'), variable('object'), $graph );
								push(@buffer, $iter->elements);
							}
							if (scalar(@buffer)) {
								return shift(@buffer);
							}
						}
					}
				}
			)->grep(sub {
				return not $seen{$_->as_string}++;
			});
		}
	}
}

=item * L<Attean::Plan::EBVFilter>

Filters results from a sub-plan based on the effective boolean value of a
named variable binding.

=cut

package Attean::Plan::EBVFilter 0.037 {
	use Moo;
	use Scalar::Util qw(blessed);
	use Types::Standard qw(Str ConsumerOf);
	use namespace::clean;
	
	with 'Attean::API::BindingSubstitutionPlan', 'Attean::API::UnaryQueryTree';
	with 'Attean::API::UnionScopeVariablesPlan';

	has 'variable' => (is => 'ro', isa => Str, required => 1);

	sub plan_as_string {
		my $self	= shift;
		return sprintf('EBVFilter { ?%s }', $self->variable);
	}
	sub tree_attributes { return qw(expression) };
	
	sub substitute_impl {
		my $self	= shift;
		my $model	= shift;
		my $bind	= shift;
		my ($impl)	= map { $_->substitute_impl($model, $bind) } @{ $self->children };
		my $var		= $self->variable;

		return sub {
			my $iter	= $impl->();
			return $iter->grep(sub {
				my $r		= shift;
				my $term	= $r->value($var);
				return 0 unless (blessed($term) and $term->does('Attean::API::Term'));
				return $term->ebv;
			});
		};
	}
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my ($impl)	= map { $_->impl($model) } @{ $self->children };
		my $var		= $self->variable;
		return sub {
			my $iter	= $impl->();
			return $iter->grep(sub {
				my $r		= shift;
				my $term	= $r->value($var);
				return 0 unless (blessed($term) and $term->does('Attean::API::Term'));
				return $term->ebv;
			});
		};
	}
}

=item * L<Attean::Plan::Merge>

Evaluates a set of sub-plans, returning the merged union of results, preserving
ordering.

=cut

package Attean::Plan::Merge 0.037 {
	use Moo;
	use Scalar::Util qw(blessed);
	use Types::Standard qw(Str ArrayRef ConsumerOf);
	use namespace::clean;
	
	with 'Attean::API::Plan', 'Attean::API::BinaryQueryTree';
	with 'Attean::API::UnionScopeVariablesPlan';

	has 'variables' => (is => 'ro', isa => ArrayRef[Str], required => 1);

	sub plan_as_string { return 'Merge' }
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my @children	= map { $_->impl($model) } @{ $self->children };
		return sub {
			die "Unimplemented";
		};
	}
}

=item * L<Attean::Plan::Union>

Evaluates a set of sub-plans, returning the union of results.

=cut

package Attean::Plan::Union 0.037 {
	use Moo;
	use Scalar::Util qw(blessed);
	use namespace::clean;
	
	with 'Attean::API::BindingSubstitutionPlan', 'Attean::API::BinaryQueryTree';
	with 'Attean::API::UnionScopeVariablesPlan';

	sub plan_as_string { return 'Union' }
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my @children	= map { $_->impl($model) } @{ $self->children };
		return $self->_impl($model, @children);
	}
	
	sub substitute_impl {
		my $self	= shift;
		my $model	= shift;
		my $b		= shift;
		unless (all { $_->does('Attean::API::BindingSubstitutionPlan') } @{ $self->children }) {
			die "Plan children do not all consume BindingSubstitutionPlan role:\n" . $self->as_string;
		}
		
		my @children	= map { $_->substitute_impl($model, $b) } @{ $self->children };
		return $self->_impl($model, @children);
	}
	
	sub _impl {
		my $self		= shift;
		my $model		= shift;
		my @children	= @_;
		my $iter_variables	= $self->in_scope_variables;

		return sub {
			if (my $current	= shift(@children)) {
				my $iter	= $current->();
				return Attean::CodeIterator->new(
					item_type => 'Attean::API::Result',
					variables => $iter_variables,
					generator => sub {
						while (blessed($iter)) {
							my $row	= $iter->next();
							if ($row) {
								return $row;
							} else {
								$current	= shift(@children);
								if ($current) {
									$iter	= $current->();
								} else {
									undef $iter;
								}
							}
						}
					},
				);
			} else {
				return Attean::ListIterator->new( item_type => 'Attean::API::Result', variables => [], values => [], );
			}
		};
	}
}

=item * L<Attean::Plan::Extend>

Evaluates a sub-plan, and extends each result by evaluating a set of
expressions, binding the produced values to new variables.

=cut

package Attean::Plan::Extend 0.037 {
	use Moo;
	use Encode;
	use UUID::Tiny ':std';
	use URI::Escape;
	use Data::Dumper;
	use I18N::LangTags;
	use POSIX qw(ceil floor);
	use Digest::SHA;
	use Digest::MD5 qw(md5_hex);
	use Scalar::Util qw(blessed looks_like_number);
	use List::Util qw(uniq all);
	use Types::Standard qw(ConsumerOf ArrayRef InstanceOf HashRef);
	use namespace::clean;

	with 'MooX::Log::Any';
	with 'Attean::API::BindingSubstitutionPlan', 'Attean::API::UnaryQueryTree';
	has 'expressions' => (is => 'ro', isa => HashRef[ConsumerOf['Attean::API::Expression']], required => 1);
	has 'active_graphs' => (is => 'ro', isa => ArrayRef[ConsumerOf['Attean::API::IRI']], required => 1);
	
	sub plan_as_string {
		my $self	= shift;
		my @strings	= map { sprintf('?%s ← %s', $_, $self->expressions->{$_}->as_string) } keys %{ $self->expressions };
		return sprintf('Extend { %s }', join(', ', @strings));
	}
	sub tree_attributes { return qw(variable expression) };
	
	sub BUILDARGS {
		my $class		= shift;
		my %args		= @_;
		my $exprs		= $args{ expressions };
		my @vars		= map { @{ $_->in_scope_variables } } @{ $args{ children } };
		my @evars		= (@vars, keys %$exprs);
		
		if (exists $args{in_scope_variables}) {
			Carp::confess "in_scope_variables is computed automatically, and must not be specified in the $class constructor";
		}
		$args{in_scope_variables}	= [@evars];
		return $class->SUPER::BUILDARGS(%args);
	}
	
	sub evaluate_expression {
		my $self	= shift;
		my $model	= shift;
		my $expr	= shift;
		my $r		= shift;
		Carp::confess unless ($expr->can('operator'));
		my $op		= $expr->operator;

		state $true			= Attean::Literal->true;
		state $false		= Attean::Literal->false;
		state $type_roles	= { qw(URI IRI IRI IRI BLANK Blank LITERAL Literal NUMERIC NumericLiteral TRIPLE Triple) };
		state $type_classes	= { qw(URI Attean::IRI IRI Attean::IRI STR Attean::Literal) };
		
		if ($expr->isa('Attean::CastExpression')) {
			my $datatype	= $expr->datatype->value;
			my ($child)	= @{ $expr->children };
			my $term	= $self->evaluate_expression($model, $child, $r);

			if ($datatype =~ m<^http://www.w3.org/2001/XMLSchema#string$>) {
				my $value	= $term->value;
				if ($term->does('Attean::API::IRI')) {
					return Attean::Literal->new(value => $term->value);
				} elsif ($term->datatype->value eq 'http://www.w3.org/2001/XMLSchema#boolean') {
					my $v	= ($value eq 'true' or $value eq '1') ? 'true' : 'false';
					return Attean::Literal->new(value => $v);
				} elsif ($term->does('Attean::API::NumericLiteral')) {
					my $v	= $term->numeric_value();
					if ($v == int($v)) {
						return Attean::Literal->new(value => int($v));
					}
				}
				
				return Attean::Literal->new(value => $value);
			}

			die "TypeError $op" unless (blessed($term) and $term->does('Attean::API::Literal'));
			if ($datatype =~ m<^http://www.w3.org/2001/XMLSchema#(integer|float|double|decimal)>) {
				my $value	= $term->value;
				my $num;
				if ($datatype eq 'http://www.w3.org/2001/XMLSchema#integer') {
					if ($term->datatype->value eq 'http://www.w3.org/2001/XMLSchema#boolean') {
						$value	= ($value eq 'true' or $value eq '1') ? '1' : '0';
					} elsif ($term->does('Attean::API::NumericLiteral')) {
						my $v	= $term->numeric_value();
						$v		=~ s/[.].*$//;
						$value	= int($v);
					} elsif ($value =~ /^[-+]\d+$/) {
						my ($v) = "$value";
						$v		=~ s/[.].*$//;
						$value	= int($v);
					}
					$num	= $value;
				} elsif ($datatype eq 'http://www.w3.org/2001/XMLSchema#decimal') {
					if ($term->datatype->value eq 'http://www.w3.org/2001/XMLSchema#boolean') {
						$value	= ($value eq 'true') ? '1' : '0';
					} elsif ($term->does('Attean::API::NumericLiteral')) {
						$value	= $term->numeric_value;
					} elsif (looks_like_number($value)) {
						if ($value =~ /[eE]/) {	# double
							die "cannot cast to xsd:decimal as precision would be lost";
						}
						$value = +$value;
					}
					$num	= "$value";
					$num	=~ s/[.]0+$/.0/;
					$num	=~ s/[.](\d+)0*$/.$1/;
				} elsif ($datatype =~ m<^http://www.w3.org/2001/XMLSchema#(float|double)$>) {
					my $typename	= $1;
					if ($term->datatype->value eq 'http://www.w3.org/2001/XMLSchema#boolean') {
						$value	= ($value eq 'true') ? '1.0' : '0.0';
					} elsif ($term->does('Attean::API::NumericLiteral')) {
						# no-op
					} elsif (looks_like_number($value)) {
						$value	= +$value;
					} else {
						die "cannot cast unrecognized value '$value' to xsd:$typename";
					}
					$num	= sprintf("%e", $value);
				}
				my $c	= Attean::Literal->new(value => $num, datatype => $expr->datatype);
				if (my $term = $c->canonicalized_term_strict()) {
					return $term;
				} else {
					die "Term value is not a valid lexical form for $datatype";
				}
			} elsif ($datatype =~ m<^http://www.w3.org/2001/XMLSchema#boolean$>) {
				if ($term->does('Attean::API::NumericLiteral')) {
					my $value	= $term->numeric_value;
					return ($value == 0) ? Attean::Literal->false : Attean::Literal->true;
				} else {
					my $value	= $term->value;
					if ($value =~ m/^(true|false|0|1)$/) {
						return ($value eq 'true' or $value eq '1') ? Attean::Literal->true : Attean::Literal->false;
					} else {
						die "Bad lexical form for xsd:boolean: '$value'";
					}
				}
			} elsif ($datatype =~ m<^http://www.w3.org/2001/XMLSchema#dateTime$>) {
				my $value	= $term->value;
				my $c	= Attean::Literal->new(value => $value, datatype => $expr->datatype);
				if ($c->does('Attean::API::DateTimeLiteral') and $c->datetime) {
					return $c;
				} else {
					die "Bad lexical form for xsd:dateTime: '$value'";
				}
			}
			$self->log->warn("Cast expression unimplemented for $datatype: " . Dumper($expr));
		} elsif ($expr->isa('Attean::ValueExpression')) {
			my $node	= $expr->value;
			if ($node->does('Attean::API::Variable')) {
				my $value	= $r->value($node->value);
				unless (blessed($value)) {
					die "Variable " . $node->as_string . " is unbound in expression " . $expr->as_string;
				}
				return $value;
			} else {
				return $node;
			}
		} elsif ($expr->isa('Attean::UnaryExpression')) {
			my ($child)	= @{ $expr->children };
			my $term	= $self->evaluate_expression($model, $child, $r);
			if ($op eq '!') {
				return ($term->ebv) ? $false : $true;
			} elsif ($op eq '-' or $op eq '+') {
				die "TypeError $op" unless (blessed($term) and $term->does('Attean::API::NumericLiteral'));
				my $v	= $term->numeric_value;
				return Attean::Literal->new( value => eval "$op$v", datatype => $term->datatype );
			}
			die "Unimplemented UnaryExpression evaluation: " . $expr->operator;
		} elsif ($expr->isa('Attean::BinaryExpression')) {
			my $op	= $expr->operator;
			if ($op eq '&&') {
				foreach my $child (@{ $expr->children }) {
					my $term	= $self->evaluate_expression($model, $child, $r);
					unless ($term->ebv) {
						return $false;
					}
				}
				return $true;
			} elsif ($op eq '||') {
				foreach my $child (@{ $expr->children }) {
					my $term	= $self->evaluate_expression($model, $child, $r);
					if (blessed($term) and $term->ebv) {
						return $true;
					}
				}
				return $false;
			} elsif ($op eq '=') {
				my ($lhs, $rhs)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
				my $eq	= $lhs->equals($rhs);
				return $eq ? $true : $false; # TODO: this may not be using value-space comparision for numerics...
			} elsif ($op eq '!=') {
				my ($lhs, $rhs)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
				return not($lhs->equals($rhs)) ? $true : $false; # TODO: this may not be using value-space comparision for numerics...
			} elsif ($op =~ m#[<>]=?#) {
				my ($lhs, $rhs)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
				my $cmp	= $lhs->compare($rhs);
				if ($cmp < 0) {
					return ($op =~ /^<=?/) ? $true : $false;
				} elsif ($cmp > 0) {
					return ($op =~ /^>=?/) ? $true : $false;
				} else {
					return ($op =~ /=/) ? $true : $false;
				}
			} elsif ($op =~ m<^[-+*/]$>) {
				my ($lhs, $rhs)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
				die "TypeError $op" unless all { blessed($_) and $_->does('Attean::API::NumericLiteral') } ($lhs, $rhs);
				my ($lv, $rv)	= map { $_->numeric_value } ($lhs, $rhs);
				my $type	= $lhs->binary_promotion_type($rhs, $op);
				if ($op eq '+') {
					return Attean::Literal->new(value => ($lv + $rv), datatype => $type);
				} elsif ($op eq '-') {
					return Attean::Literal->new(value => ($lv - $rv), datatype => $type);
				} elsif ($op eq '*') {
					return Attean::Literal->new(value => ($lv * $rv), datatype => $type);
				} elsif ($op eq '/') {
					return Attean::Literal->new(value => ($lv / $rv), datatype => $type);
				}
			}
			
			$self->log->warn("Binary operator $op expression evaluation unimplemented: " . Dumper($expr));
			die "Expression evaluation unimplemented: " . $expr->as_string;
		} elsif ($expr->isa('Attean::FunctionExpression')) {
			my $func	= $expr->operator;
			if ($func eq 'IF') {
				my ($check, @children)	= @{ $expr->children };
				my ($term)		= $self->evaluate_expression($model, $check, $r);
				$self->log->warn($@) if ($@);
				my $expr	= $children[ (blessed($term) and $term->ebv) ? 0 : 1 ];
				my $value	= $self->evaluate_expression($model, $expr, $r);
# 				warn '############# ' . $value->as_string;
				return $value;
			} elsif ($func eq 'COALESCE') {
# 				warn "COALESCE: . " . $r->as_string . "\n";
				foreach my $child (@{ $expr->children }) {
# 					warn '- ' . $child->as_string . "\n";
					my $term	= eval { $self->evaluate_expression($model, $child, $r) };
# 					warn $@ if $@;
					if (blessed($term)) {
# 						warn '    returning ' . $term->as_string . "\n";
						return $term;
					}
				}
# 				warn "    no value\n";
				return;
			} elsif ($func eq 'BOUND') {
				my ($child)	= @{ $expr->children };
				my ($term)	= eval { $self->evaluate_expression($model, $child, $r) };
				return blessed($term) ? $true : $false;
			}
			
			if ($func eq 'INVOKE') {
				my @children	= @{ $expr->children };
				my $furi		= $self->evaluate_expression($model, shift(@children), $r)->value;
				if (my $func = Attean->get_global_function($furi)) {
					my @operands	= map { $self->evaluate_expression($model, $_, $r) } @children;
					my $rr	= eval { $func->($model, $self->active_graphs, @operands) };
					if ($@) {
						warn "INVOKE error: $@" if $@;
						return;
					}
					return $rr;
				} elsif (my $fform = Attean->get_global_functional_form($furi)) {
					my @operands	= map { eval { $self->evaluate_expression($model, $_, $r) } || undef } @children;
					my $rr	= eval { $fform->($model, $self->active_graphs, @operands) };
					if ($@) {
						warn "INVOKE error: $@" if $@;
						return $r;
					}
					return $rr;
				} else {
					die "No extension registered for <$furi>";
				}
			} else {
				my @terms	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
				if ($func =~ /^IS([UI]RI|BLANK|LITERAL|NUMERIC|TRIPLE)$/) {
					my $role	= "Attean::API::$type_roles->{$1}";
					my $t		= shift(@terms);
					my $ok		= (blessed($t) and $t->does($role));
					return $ok ? $true : $false;
				} elsif ($func eq 'REGEX') {
					my ($string, $pattern, $flags)	= @terms;
	# 				my ($string, $pattern, $flags)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					# TODO: ensure that $string is a literal
					($string, $pattern, $flags)	= map { blessed($_) ? $_->value : '' } ($string, $pattern, $flags);
					my $re;
					if ($flags =~ /i/) {
						$re	= qr/$pattern/i;
					} else {
						$re	= qr/$pattern/;
					}
					return ($string =~ $re) ? $true : $false;
				} elsif ($func =~ /^(NOT)?IN$/) {
					my $ok			= ($func eq 'IN') ? $true : $false;
					my $notok		= ($func eq 'IN') ? $false : $true;
	# 				my @children	= @{ $expr->children };
					my ($term, @children)	= @terms;
	# 				my ($term)		= $self->evaluate_expression($model, shift(@children), $r);
	# 				foreach my $child (@{ $expr->children }) {
					foreach my $value (@children) {
	# 					my $value	= $self->evaluate_expression($model, $child, $r);
						if ($term->equals($value)) {
							return $ok;
						}
					}
					return $notok;
				} elsif ($func eq 'NOW') {
					my $dt		= DateTime->now;
					my $value	= DateTime::Format::W3CDTF->new->format_datetime( $dt );
					return Attean::Literal->new(value => $value, datatype => 'http://www.w3.org/2001/XMLSchema#dateTime');
				} elsif ($func eq 'STR') {
					my ($term)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					return Attean::Literal->new(value => $term->value);
				} elsif ($func =~ /^[UI]RI$/) { # IRI URI
					my ($term)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					return Attean::IRI->new(value => $term->value, base => $expr->base);
				} elsif ($func eq 'ABS') {
					my ($string)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					my $value		= abs($string->numeric_value);
					return Attean::Literal->new(value => $value, datatype => $string->datatype);
				} elsif ($func eq 'ROUND') {
					my ($string)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					my $value		= $string->numeric_value;
					my $mult	= 1;
					if ($value < 0) {
						$mult	= -1;
						$value	= -$value;
					}
					my $round	= $mult * POSIX::floor($value + 0.50000000000008);
					return Attean::Literal->new(value => $round, datatype => $string->datatype);
				} elsif ($func eq 'CEIL') {
					my ($string)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					my $value		= ceil($string->numeric_value);
					return Attean::Literal->new(value => $value, datatype => $string->datatype);
				} elsif ($func eq 'FLOOR') {
					my ($string)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					my $value		= floor($string->numeric_value);
					return Attean::Literal->new(value => $value, datatype => $string->datatype);
				} elsif ($func eq 'CONCAT') {
					my @strings		= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					if (scalar(@strings) == 0) {
						return Attean::Literal->new(value => '');
					}
	# 				die "CONCAT called with terms that are not argument compatible" unless ($strings[0]->argument_compatible(@strings));
					my %args;
					if (my $l = $strings[0]->language) {
						$args{language}	= $l;
					} else {
						my $dt	= $strings[0]->datatype;
						if ($dt->value eq '') {
							$args{datatype}	= 'http://www.w3.org/2001/XMLSchema#string';
						}
					}
					foreach my $s (@strings) {
						die unless ($s->does('Attean::API::Literal'));
						die if ($s->datatype and not($s->datatype->value =~ m<http://www.w3.org/(1999/02/22-rdf-syntax-ns#langString|2001/XMLSchema#string)>));
						if (my $l2 = $s->language) {
							if (my $l1 = $args{language}) {
								if ($l1 ne $l2) {
									delete $args{language};
								}
							}
						} else {
							delete $args{language};
						}
					}
					my $c	= Attean::Literal->new(value => join('', map { $_->value } @strings), %args);
					return $c;
				} elsif ($func eq 'DATATYPE') {
					my ($string)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					die unless ($string->does('Attean::API::Literal'));
					return $string->datatype;
				} elsif ($func eq 'LANG') {
					my ($string)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					die unless ($string->does('Attean::API::Literal'));
					my $value		= $string->language // '';
					return Attean::Literal->new(value => $value);
				} elsif ($func eq 'LANGMATCHES') {
					my ($term, $pat)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					my $lang	= $term->value;
					my $match	= $pat->value;
					if ($match eq '*') {
						# """A language-range of "*" matches any non-empty language-tag string."""
						return $lang ? $true : $false;
					} else {
						return (I18N::LangTags::is_dialect_of( $lang, $match )) ? $true : $false;
					}
				
				} elsif ($func eq 'ENCODE_FOR_URI') {
					my ($string)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					return Attean::Literal->new(value => uri_escape_utf8($string->value));
				} elsif ($func =~ /^[LU]CASE$/) {
					my $term		= shift(@terms);
					my $value		= ($func eq 'LCASE') ? lc($term->value) : uc($term->value);
					return Attean::Literal->new(value => $value, $term->construct_args);
				} elsif ($func eq 'STRLANG') {
					my ($term, $lang)	= @terms;
					die unless ($term->does('Attean::API::Literal'));
					die unless ($term->datatype->value =~ m<http://www.w3.org/(1999/02/22-rdf-syntax-ns#langString|2001/XMLSchema#string)>);
					die if ($term->language);
					return Attean::Literal->new(value => $term->value, language => $lang->value);
				} elsif ($func eq 'STRDT') {
					my ($term, $dt)	= @terms;
					die unless ($term->does('Attean::API::Literal'));
					die unless ($term->datatype->value =~ m<http://www.w3.org/(1999/02/22-rdf-syntax-ns#langString|2001/XMLSchema#string)>);
					die if ($term->language);
	# 				my ($term, $dt)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					return Attean::Literal->new(value => $term->value, datatype => $dt->value);
				} elsif ($func eq 'REPLACE') {
					my ($term, $pat, $rep)	= @terms;
					die unless ($term->does('Attean::API::Literal'));
					die unless ($term->language or $term->datatype->value =~ m<http://www.w3.org/(1999/02/22-rdf-syntax-ns#langString|2001/XMLSchema#string)>);
	# 				my ($term, $pat, $rep)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					my $value	= $term->value;
					my $pattern	= $pat->value;
					my $replace	= $rep->value;
					die 'REPLACE() called with unsafe ?{} match pattern' if (index($pattern, '(?{') != -1 or index($pattern, '(??{') != -1);
					die 'REPLACE() called with unsafe ?{} replace pattern' if (index($replace, '(?{') != -1 or index($replace, '(??{') != -1);

					$replace	=~ s/\\/\\\\/g;
					$replace	=~ s/\$(\d+)/\$$1/g;
					$replace	=~ s/"/\\"/g;
					$replace	= qq["$replace"];
					no warnings 'uninitialized';
					$value	=~ s/$pattern/"$replace"/eeg;
				# 	warn "==> " . Dumper($value);
					return Attean::Literal->new(value => $value, $term->construct_args);
				} elsif ($func eq 'SUBSTR') {
					my ($term, @args)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					my $value	= $term->value;
					my @nums;
					foreach my $i (0 .. $#args) {
						my $argnum	= $i + 2;
						my $arg		= $args[ $i ];
						push(@nums, $arg->numeric_value);
					}
					$nums[0]--;
					my $substring	= (scalar(@nums) > 1) ? substr($value, $nums[0], $nums[1]) : substr($value, $nums[0]);
					return Attean::Literal->new(value => $substring, $term->construct_args);
				} elsif ($func eq 'CONTAINS') {
					my ($term, $pattern)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					if ($term->has_language and $pattern->has_language) {
						if ($term->literal_value_language ne $pattern->literal_value_language) {
							die "CONTAINS called with literals of different languages";
						}
					}
					my ($string, $pat)	= map { $_->value } ($term, $pattern);
					my $pos		= index($string, $pat);
					return ($pos >= 0) ? $true : $false;
				} elsif ($func eq 'STRSTARTS') {
					my (@terms)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					my ($string, $pat)	= map { $_->value } @terms;
					return (substr($string, 0, length($pat)) eq $pat) ? $true : $false;
				} elsif ($func eq 'STRENDS') {
					my (@terms)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					my ($string, $pat)	= map { $_->value } @terms;
					return (substr($string, length($string) - length($pat)) eq $pat) ? $true : $false;
				} elsif ($func eq 'STRAFTER') {
					my ($term, $pat)	= @terms;
					die "STRAFTER called without a literal" unless ($term->does('Attean::API::Literal'));
					die "STRAFTER called without a plain literal" unless ($term->language or $term->datatype->value eq 'http://www.w3.org/2001/XMLSchema#string');
					die "$func arguments are not term compatible: " . join(', ', map { $_->as_string } @terms) unless ($term->argument_compatible($pat));
					# TODO: check that the terms are argument compatible
					my $value	= $term->value;
					my $match	= $pat->value;
					my $i		= index($value, $match, 0);
					if ($i < 0) {
						return Attean::Literal->new(value => '');
					} else {
						return Attean::Literal->new(value => substr($value, $i+length($match)), $term->construct_args);
					}
				} elsif ($func eq 'STRBEFORE') {
					my ($term, $pat)	= @terms;
					die "STRBEFORE called without a literal" unless ($term->does('Attean::API::Literal'));
					die "STRBEFORE called without a plain literal" unless ($term->language or $term->datatype->value eq 'http://www.w3.org/2001/XMLSchema#string');
					die "$func arguments are not term compatible: " . join(', ', map { $_->as_string } @terms) unless ($term->argument_compatible($pat));
					# TODO: check that the terms are argument compatible
					my $value	= $term->value;
					my $match	= $pat->value;
					my $i		= index($value, $match, 0);
					if ($i < 0) {
						return Attean::Literal->new(value => '');
					} else {
						return Attean::Literal->new(value => substr($value, 0, $i), $term->construct_args);
					}
				} elsif ($func eq 'STRLEN') {
					my ($string)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					return Attean::Literal->new(value => length($string->value), datatype => 'http://www.w3.org/2001/XMLSchema#integer');
				} elsif ($func eq 'MD5') {
					my ($string)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					my $bytes		= encode('UTF-8', $string->value, Encode::FB_CROAK);
					return Attean::Literal->new(value => md5_hex($bytes));
				} elsif ($func =~ /^SHA(\d+)$/) {
					my $sha	= Digest::SHA->new($1);
					my ($string)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					my $bytes		= encode('UTF-8', $string->value, Encode::FB_CROAK);
					$sha->add($bytes);
					return Attean::Literal->new(value => $sha->hexdigest);
				} elsif ($func eq 'RAND') {
					return Attean::Literal->new(value => rand(), datatype => 'http://www.w3.org/2001/XMLSchema#double');
				} elsif ($func =~ /^(YEAR|MONTH|DAY|HOUR|MINUTE)S?$/) {
					my $method	= lc($1);
					my ($term)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					my $dt		= $term->datetime;
					return Attean::Literal->new(value => $dt->$method(), datatype => 'http://www.w3.org/2001/XMLSchema#integer');
				} elsif ($func eq 'SECONDS') {
					my ($term)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					my $dt		= $term->datetime;
					return Attean::Literal->new(value => $dt->second, datatype => 'http://www.w3.org/2001/XMLSchema#decimal');
				} elsif ($func eq 'TIMEZONE') {
					my ($term)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					my $dt		= $term->datetime;
					my $tz		= $dt->time_zone;
					die "TIMEZONE called with a dateTime without a timezone" if ($tz->is_floating);
					my $offset	= $tz->offset_for_datetime( $dt );
					my $minus	= '';
					if ($offset < 0) {
						$minus	= '-';
						$offset	= -$offset;
					}

					my $duration	= "${minus}PT";
					if ($offset >= 60*60) {
						my $h	= int($offset / (60*60));
						$duration	.= "${h}H" if ($h > 0);
						$offset	= $offset % (60*60);
					}
					if ($offset >= 60) {
						my $m	= int($offset / 60);
						$duration	.= "${m}M" if ($m > 0);
						$offset	= $offset % 60;
					}
					my $s	= int($offset);
					$duration	.= "${s}S" if ($s > 0 or $duration eq 'PT');
			
					return Attean::Literal->new(value => $duration, datatype => 'http://www.w3.org/2001/XMLSchema#dayTimeDuration');
				} elsif ($func eq 'TZ') {
					my ($term)	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					my $dt		= $term->datetime;
					my $tz		= $dt->time_zone;
					return Attean::Literal->new(value =>'') if ($tz->is_floating);
					return Attean::Literal->new('Z') if ($tz->is_utc);
					my $offset	= $tz->offset_for_datetime( $dt );
					my $hours	= 0;
					my $minutes	= 0;
					my $minus	= '+';
					if ($offset < 0) {
						$minus	= '-';
						$offset	= -$offset;
					}

					if ($offset >= 60*60) {
						$hours	= int($offset / (60*60));
						$offset	= $offset % (60*60);
					}
					if ($offset >= 60) {
						$minutes	= int($offset / 60);
						$offset	= $offset % 60;
					}
					my $seconds	= int($offset);
					return Attean::Literal->new(value => sprintf('%s%02d:%02d', $minus, $hours, $minutes));
				} elsif ($func eq 'UUID') {
					my $uuid	= 'urn:uuid:' . uc(uuid_to_string(create_uuid()));
					return Attean::IRI->new(value => $uuid);
				} elsif ($func eq 'STRUUID') {
					return Attean::Literal->new(value => uc(uuid_to_string(create_uuid())));
				} elsif ($func eq 'BNODE') {
					if (scalar(@{ $expr->children })) {
						my $string	= $self->evaluate_expression($model, $expr->children->[0], $r);
						my $value	= $string->value;
						my $b		= (exists $r->eval_stash->{'sparql:bnode'}{$value})
									? $r->eval_stash->{'sparql:bnode'}{$value}
									: Attean::Blank->new();
						$r->eval_stash->{'sparql:bnode'}{$value}	= $b;
						return $b;
					} else {
						return Attean::Blank->new();
					}
				} elsif ($func eq 'SAMETERM') {
					my @operands	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					my ($a, $b)	= @operands;
					die "TypeError: SAMETERM" unless (blessed($operands[0]) and blessed($operands[1]));
					if ($a->does('Attean::API::Binding')) {
						my $ok	= ($a->sameTerms($b));
						return $ok ? $true : $false;
					} else {
						my $ok	= ($a->value eq $b->value);
						return $ok ? $true : $false;
					}
				} elsif ($func =~ /^(SUBJECT|PREDICATE|OBJECT)$/) {
					my @operands	= map { $self->evaluate_expression($model, $_, $r) } @{ $expr->children };
					my $pos	= lc($func);
					my $term	= $operands[0]->$pos();
					return $term;
				} else {
					warn "Expression evaluation unimplemented: " . $expr->as_string;
					$self->log->warn("Expression evaluation unimplemented: " . $expr->as_string);
					die "Expression evaluation unimplemented: " . $expr->as_string;
				}
			}
		} elsif ($expr->isa('Attean::ExistsPlanExpression')) {
			my $plan	= $expr->plan;
			my $impl	= $plan->substitute_impl($model, $r);
			my $iter	= $impl->();
			my $found	= 0;
			if (my $row = $iter->next) {
# 				warn "EXISTS found row: " . $row->as_string;
				$found++;
			}
			
			return $found ? Attean::Literal->true : Attean::Literal->false;
		} else {
			$self->log->warn("Expression evaluation unimplemented: " . $expr->as_string);
			die "Expression evaluation unimplemented: " . $expr->as_string;
		}
	}
	
	sub substitute_impl {
		my $self	= shift;
		my $model	= shift;
		my $bind	= shift;
		my %exprs	= %{ $self->expressions };
		my ($impl)	= map { $_->substitute_impl($model, $bind) } @{ $self->children };
		# TODO: substitute variables in the expression
		return $self->_impl($model, $impl, %exprs);
	}
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my %exprs	= %{ $self->expressions };
		my ($impl)	= map { $_->impl($model) } @{ $self->children };
		return $self->_impl($model, $impl, %exprs);
	}
	
	sub _impl {
		my $self	= shift;
		my $model	= shift;
		my $impl	= shift;
		my %exprs	= @_;
		my $iter_variables	= $self->in_scope_variables;

		return sub {
			my $iter	= $impl->();
			return Attean::CodeIterator->new(
				item_type => 'Attean::API::Result',
				variables => $iter_variables,
				generator => sub {
					ROW: while (my $r = $iter->next) {
# 						warn 'Extend Row -------------------------------> ' . $r->as_string . "\n";
						my %row	= map { $_ => $r->value($_) } $r->variables;
						foreach my $var (keys %exprs) {
							my $expr	= $exprs{$var};
# 							warn "-> $var => "  . $expr->as_string . "\n";
							my $term	= eval { $self->evaluate_expression($model, $expr, $r) };
# 							if ($@) {
# 								warn "EXTEND expression evaluation error: $@";
# 								warn '- expression: ' . $expr->as_string;
# 							}
							if (blessed($term)) {
# 								warn "===> " . $term->as_string . "\n";
								if ($row{ $var } and $term->as_string ne $row{ $var }->as_string) {
									next ROW;
								}
								
								if ($term->does('Attean::API::Binding')) {
									# patterns need to be made ground to be bound as values (e.g. TriplePattern -> Triple)
									$term	= $term->ground($r);
								}
								
								$row{ $var }	= $term;
							}
						}
						return Attean::Result->new( bindings => \%row, eval_stash => $r->eval_stash );
					}
					return;
				}
			);
		};
	}
}

=item * L<Attean::Plan::HashDistinct>

Evaluates a sub-plan, and returns distinct results by checking a persistent
hash of already-seen results.

=cut

package Attean::Plan::HashDistinct 0.037 {
	use Moo;
	use namespace::clean;
	
	with 'Attean::API::Plan', 'Attean::API::UnaryQueryTree';
	with 'Attean::API::UnionScopeVariablesPlan';

	sub plan_as_string { return 'HashDistinct' }
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my ($impl)	= map { $_->impl($model) } @{ $self->children };
		my %seen;
		return sub {
			my $iter	= $impl->();
			return $iter->grep(sub { return not($seen{ shift->as_string }++); });
		};
	}
}

=item * L<Attean::Plan::Unique>

Evaluates an already-ordered sub-plan, and returns distinct results by
filtering out sequential duplicates.

=cut

package Attean::Plan::Unique 0.037 {
	use Moo;
	use namespace::clean;
	
	with 'Attean::API::Plan', 'Attean::API::UnaryQueryTree';
	with 'Attean::API::UnionScopeVariablesPlan';

	sub plan_as_string { return 'Unique' }
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my ($impl)	= map { $_->impl($model) } @{ $self->children };
		return sub {
			my $iter	= $impl->();
			my $last	= '';
			return $iter->grep(sub {
				my $r	= shift;
				my $s	= $r->as_string;
				my $ok	= $s ne $last;
				$last	= $s;
				return $ok;
			});
		};
	}
}

=item * L<Attean::Plan::Slice>

Evaluates a sub-plan, and returns the results after optionally skipping some
number of results ("offset") and limiting the total number of returned results
("limit").

=cut

package Attean::Plan::Slice 0.037 {
	use Moo;
	use Types::Standard qw(Int);
	use namespace::clean;
	
	with 'Attean::API::Plan', 'Attean::API::UnaryQueryTree';
	with 'Attean::API::UnionScopeVariablesPlan';

	has 'limit' => (is => 'ro', isa => Int, default => -1);
	has 'offset' => (is => 'ro', isa => Int, default => 0);

	sub plan_as_string {
		my $self	= shift;
		my @str;
		push(@str, "Limit=" . $self->limit) if ($self->limit >= 0);
		push(@str, "Offset=" . $self->offset) if ($self->offset > 0);
		return sprintf('Slice { %s }', join(' ', @str));
	}
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my ($impl)	= map { $_->impl($model) } @{ $self->children };
		my $offset	= $self->offset;
		my $limit	= $self->limit;
		return sub {
			my $iter	= $impl->();
			$iter		= $iter->offset($offset) if ($offset > 0);
			$iter		= $iter->limit($limit) if ($limit >= 0);
			return $iter;
		};
	}
}

=item * L<Attean::Plan::Project>

Evaluates a sub-plan and returns projected results by only keeping a fixed-set
of variable bindings in each result.

=cut

package Attean::Plan::Project 0.037 {
	use Moo;
	with 'Attean::API::BindingSubstitutionPlan', 'Attean::API::UnaryQueryTree';
	use Types::Standard qw(ArrayRef ConsumerOf);
	has 'variables' => (is => 'ro', isa => ArrayRef[ConsumerOf['Attean::API::Variable']], required => 1);

	sub BUILDARGS {
		my $class		= shift;
		my %args		= @_;
		my @vars		= map { $_->value } @{ $args{variables} };
		
		if (exists $args{in_scope_variables}) {
			Carp::confess "in_scope_variables is computed automatically, and must not be specified in the $class constructor";
		}
		$args{in_scope_variables}	= \@vars;

		return $class->SUPER::BUILDARGS(%args);
	}
	
# 	sub BUILD {
# 		my $self	= shift;
# 		my @vars	= map { $_->value } @{ $self->variables };
# 		unless (scalar(@vars)) {
# 			Carp::confess "No vars in project?";
# 		}
# 	}
	
	sub plan_as_string {
		my $self	= shift;
		return sprintf('Project { %s }', join(' ', map { '?' . $_->value } @{ $self->variables }));
	}
	sub tree_attributes { return qw(variables) };
	
	sub substitute_impl {
		my $self	= shift;
		my $model	= shift;
		my $bind	= shift;
		my ($impl)	= map { $_->substitute_impl($model, $bind) } @{ $self->children };
		my @vars	= map { $_->value } @{ $self->variables };
		my $iter_variables	= $self->in_scope_variables;

		# TODO: substitute variables in the projection where appropriate
		return sub {
			my $iter	= $impl->();
			return $iter->map(sub {
				my $r	= shift;
				my $b	= { map { my $t	= $r->value($_); $t	? ($_ => $t) : () } @vars };
				return Attean::Result->new( bindings => $b );
			}, $iter->item_type, variables => $iter_variables);
		};
	}
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my ($impl)	= map { $_->impl($model) } @{ $self->children };
		my @vars	= map { $_->value } @{ $self->variables };
		my $iter_variables	= $self->in_scope_variables;

		return sub {
			my $iter	= $impl->();
			return $iter->map(sub {
				my $r	= shift;
				my $b	= { map { my $t	= $r->value($_); $t	? ($_ => $t) : () } @vars };
				return Attean::Result->new( bindings => $b );
			}, $iter->item_type, variables => $iter_variables);
		};
	}
}

=item * L<Attean::Plan::OrderBy>

Evaluates a sub-plan and returns the results after fully materializing and
sorting is applied.

=cut

package Attean::Plan::OrderBy 0.037 {
	use Moo;
	use Types::Standard qw(HashRef ArrayRef InstanceOf Bool Str);
	use Scalar::Util qw(blessed);
	use namespace::clean;
	
	with 'Attean::API::Plan', 'Attean::API::UnaryQueryTree';
	with 'Attean::API::UnionScopeVariablesPlan';

	has 'variables' => (is => 'ro', isa => ArrayRef[Str], required => 1);
	has 'ascending' => (is => 'ro', isa => HashRef[Bool], required => 1);

	sub plan_as_string {
		my $self	= shift;
		my @vars	= @{ $self->variables };
		my $ascending	= $self->ascending;
		my @strings	= map { sprintf('%s(?%s)', ($ascending->{$_} ? 'ASC' : 'DESC'), $_) } @vars;
		return sprintf('Order { %s }', join(', ', @strings));
	}
	
	sub sort_rows {
		my $self		= shift;
		my $vars		= shift;
		my $ascending	= shift;
		my $rows		= shift;
		local($Attean::API::Binding::ALLOW_IRI_COMPARISON)	= 1;
		my @sorted		= map { $_->[0] } sort {
			my ($ar, $avalues)	= @$a;
			my ($br, $bvalues)	= @$b;
			my $c	= 0;
			foreach my $i (0 .. $#{ $vars }) {
				my $ascending	= $ascending->{ $vars->[$i] };
				my ($av, $bv)	= map { $_->[$i] } ($avalues, $bvalues);

				# Mirrors code in Attean::SimpleQueryEvaluator->evaluate
				if (blessed($av) and $av->does('Attean::API::Binding') and (not(defined($bv)) or not($bv->does('Attean::API::Binding')))) {
					$c	= 1;
				} elsif (blessed($bv) and $bv->does('Attean::API::Binding') and (not(defined($av)) or not($av->does('Attean::API::Binding')))) {
					$c	= -1;
				} else {
					$c		= eval { $av ? $av->compare($bv) : 1 };
					if ($@) {
						$c	= 1;
					}
				}
				$c		*= -1 unless ($ascending);
				last unless ($c == 0);
			}
			$c
		} map {
			my $r = $_;
			[$r, [map { $r->value($_) } @$vars]]
		} @$rows;
		return @sorted;
	}
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my $vars	= $self->variables;
		my $ascending	= $self->ascending;
		my ($impl)	= map { $_->impl($model) } @{ $self->children };
		my $iter_variables	= $self->in_scope_variables;

		return sub {
			my $iter	= $impl->();
			my @rows	= $iter->elements;
			my @sorted	= $self->sort_rows($vars, $ascending, \@rows);
			return Attean::ListIterator->new(
				values => \@sorted,
				variables => $iter_variables,
				item_type => $iter->item_type
			);
		}
	}
}

=item * L<Attean::Plan::Service>

Evaluates a SPARQL query against a remote endpoint.

=cut

package Attean::Plan::Service 0.037 {
	use Moo;
	use Types::Standard qw(ConsumerOf Bool Str InstanceOf);
	use Encode qw(encode);
	use Scalar::Util qw(blessed);
	use URI::Escape;
	use Attean::SPARQLClient;
	use namespace::clean;

	with 'Attean::API::Plan', 'Attean::API::UnaryQueryTree';

	has 'endpoint' => (is => 'ro', isa => ConsumerOf['Attean::API::TermOrVariable'], required => 1);
	has 'silent' => (is => 'ro', isa => Bool, default => 0);
	has 'sparql' => (is => 'ro', isa => Str, required => 1);
	has 'user_agent' => (is => 'rw', isa => InstanceOf['LWP::UserAgent']);
	has 'request_signer' => (is => 'rw');
	has 'client' => (is => 'rw', required => 0);

	sub plan_as_string {
		my $self	= shift;
		my $sparql	= $self->sparql;
		$sparql		=~ s/\s+/ /g;
		return sprintf('Service <%s> %s', $self->endpoint->as_string, $sparql);
	}
	
	sub tree_attributes { return qw(endpoint) };
	sub impl {
		my $self	= shift;
		my $model	= shift;

		my $endpoint	= $self->endpoint->value;
		my $sparql		= $self->sparql;
		my $silent		= $self->silent;
		my %args		= (
			endpoint		=> $endpoint,
			silent			=> $silent,
			request_signer	=> $self->request_signer,
		);
		$args{user_agent}	= $self->user_agent if ($self->user_agent);
		my $client		= $self->client || Attean::SPARQLClient->new(%args);
		return sub {
			return $client->query($sparql);
		};
	}
}

=item * L<Attean::Plan::Table>

Returns a constant set of results.

=cut

package Attean::Plan::Table 0.037 {
	use Moo;
	use Types::Standard qw(ArrayRef ConsumerOf);
	use namespace::clean;

	with 'Attean::API::Plan', 'Attean::API::UnaryQueryTree';

	has variables => (is => 'ro', isa => ArrayRef[ConsumerOf['Attean::API::Variable']]);
	has rows => (is => 'ro', isa => ArrayRef[ConsumerOf['Attean::API::Result']]);

	sub tree_attributes { return qw(variables rows) };
	sub plan_as_string {
		my $self	= shift;
		my $level	= shift;
		my $indent	= '  ' x ($level + 1);
		my $vars	= join(', ', map { "?$_" } @{ $self->in_scope_variables });
		my $s		= "Table (" . $vars . ")";
		foreach my $row (@{ $self->rows }) {
			$s	.= "\n-${indent} " . $row->as_string;
		}
		return $s;
	}
	
	sub BUILDARGS {
		my $class		= shift;
		my %args		= @_;
		my @vars		= map { $_->value } @{ $args{variables} };
		
		if (exists $args{in_scope_variables}) {
			Carp::confess "in_scope_variables is computed automatically, and must not be specified in the $class constructor";
		}
		$args{in_scope_variables}	= \@vars;

		return $class->SUPER::BUILDARGS(%args);
	}
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my $rows	= $self->rows;
		my $iter_variables	= $self->in_scope_variables;

		return sub {
			return Attean::ListIterator->new(
				item_type => 'Attean::API::Result',
				variables => $iter_variables,
				values => $rows
			);
		};
	}
}

=item * L<Attean::Plan::Iterator>

Returns a constant set of results.

Be aware that if the iterator being wrapped is not repeatable (consuming the
L<Attean::API::RepeatableIterator> role), then this plan may only be evaluated
once.

A size estimate may be given if it is available. If the iterator is an
L<Attean::ListIterator>, the size of that iterator will be used.

=cut

package Attean::Plan::Iterator 0.037 {
	use Moo;
	use Types::Standard qw(ArrayRef ConsumerOf Int);
	use namespace::clean;

	with 'Attean::API::Plan', 'Attean::API::UnaryQueryTree';

	has iterator		=> (is => 'ro', isa => ConsumerOf['Attean::API::ResultIterator']);
	has size_estimate	=> (is => 'lazy', isa => Int, predicate => 1);

	sub _build_size_estimate {
		my $self = shift;
		my $iter = $self->iterator;
		if ($iter->isa('Attean::ListIterator')) {
			return $iter->size;
		}
	}


	sub tree_attributes { return qw(iterator) };
	sub plan_as_string {
		my $self	= shift;
		my $level	= shift;
		my $indent	= '  ' x ($level + 1);
		my $string = 'Iterator (';
		$string	.= join(', ', map { "?$_" } @{ $self->in_scope_variables });
		if ($self->has_size_estimate) {
			$string .= ' with ' . $self->size_estimate . ' elements';
		}
		$string	.= ')';
		return $string;
	}
	
	sub BUILDARGS {
		my $class		= shift;
		my %args		= @_;
		my $vars		= $args{iterator}->variables;
		
		if (exists $args{in_scope_variables}) {
			Carp::confess "in_scope_variables is computed automatically, and must not be specified in the $class constructor";
		}
		$args{in_scope_variables}	= $vars;

		return $class->SUPER::BUILDARGS(%args);
	}
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my $iter	= $self->iterator;

		return sub {
			if ($iter->does('Attean::API::RepeatableIterator')) {
				$iter->reset;
			}
			return $iter;
		};
	}
}

=item * L<Attean::Plan::ALPPath>

=cut

package Attean::Plan::ALPPath 0.037 {
	use Moo;
	use Attean::TreeRewriter;
	use Types::Standard qw(ArrayRef ConsumerOf);
	use namespace::clean;

	has 'subject'		=> (is => 'ro', required => 1);
	has 'object'		=> (is => 'ro', required => 1);
	has 'graph'			=> (is => 'ro', required => 1);
	has 'step_begin'	=> (is => 'ro', required => 1);
	has 'step_end'		=> (is => 'ro', required => 1);
	has 'skip'			=> (is => 'ro', required => 1, default => 0);
# 	has 'children'		=> (is => 'ro', isa => ConsumerOf['Attean::API::BindingSubstitutionPlan'], required => 1);
	
	with 'Attean::API::BindingSubstitutionPlan', 'Attean::API::NullaryQueryTree';

	sub tree_attributes { return qw(subject object graph) };
	with 'Attean::API::Plan', 'Attean::API::UnaryQueryTree';
	
	sub plan_as_string {
		my $self	= shift;
		my @strings;
		push(@strings, sprintf('%s ← %s', map { $_->ntriples_string } ($self->subject, $self->step_begin)));
		push(@strings, sprintf('%s ← %s', map { $_->ntriples_string } ($self->object, $self->step_end)));
		return sprintf('ALPPath %s', join(', ', @strings));
	}
	
	sub BUILDARGS {
		my $class		= shift;
		my %args		= @_;
		my @vars		= map { $_->value } grep { $_->does('Attean::API::Variable') } (@args{qw(subject object)});
		
		if (exists $args{in_scope_variables}) {
			Carp::confess "in_scope_variables is computed automatically, and must not be specified in the $class constructor";
		}
		$args{in_scope_variables}	= \@vars;

		return $class->SUPER::BUILDARGS(%args);
	}
	
	sub alp {
		my $model	= shift;
		my $graph	= shift;
		my $skip	= shift;
		my $x		= shift;
		my $path	= shift;
		my $v		= shift;
		my $start	= shift;
		my $end		= shift;
		my $bind	= shift;
		if (exists $v->{$x->as_string}) {
			return;
		}
		my $binding	= Attean::Result->new( bindings => { $start => $x } )->join($bind);
		unless ($binding) {
			return;
		}
		
		if ($skip) {
			$skip--;
		} else {
			$v->{$x->as_string}	= $x;
		}
		
		my $impl	= $path->substitute_impl($model, $binding);
		my $iter	= $impl->();
		while (my $row = $iter->next()) {
			my $n	= $row->value($end);
			alp($model, $graph, $skip, $n, $path, $v, $start, $end, $bind);
		}
	}
	
	sub substitute_impl {
		my $self	= shift;
		my $model	= shift;
		my $bind	= shift;
		my $path	= $self->children->[0];
		my $subject	= $self->subject;
		my $object	= $self->object;
		my $graph	= $self->graph;
		my $start	= $self->step_begin->value;
		my $end		= $self->step_end->value;
		my $skip	= $self->skip;
		my $iter_variables	= $self->in_scope_variables;

		for ($subject, $object) {
			if ($_->does('Attean::API::Variable')) {
				my $name	= $_->value;
				if (my $node = $bind->value($name)) {
					$_	= $node;
				}
			}
		}
		
		my $s_var	= $subject->does('Attean::API::Variable');
		my $o_var	= $object->does('Attean::API::Variable');
		if ($s_var and $o_var) {
			return sub {
				my $nodes	= $model->graph_nodes($graph);
				my @rows;
				while (my $n = $nodes->next) {
					my %seen;
					alp($model, $graph, $skip, $n, $path, \%seen, $start, $end, $bind);
					foreach my $term (values %seen) {
						my $b	= Attean::Result->new( bindings => {
							$subject->value => $n,
							$object->value => $term,
						} );
						push(@rows, $b);
					}
				}
				return Attean::ListIterator->new(
					item_type => 'Attean::API::Result',
					variables => $iter_variables,
					values => \@rows,
				);
			};
		} elsif ($o_var) {
			return sub {
				my %seen;
				alp($model, $graph, $skip, $subject, $path, \%seen, $start, $end, $bind);
				my @rows	= map { Attean::Result->new( bindings => { $object->value => $_ } ) } (values %seen);
				return Attean::ListIterator->new(
					item_type => 'Attean::API::Result',
					variables => $iter_variables,
					values => \@rows,
				);
			};
		} elsif ($s_var) {
			die "ALP for FB should never occur in a plan (should be inversed during planning)";
		} else {
			return sub {
				my %seen;
				alp($model, $graph, $skip, $subject, $path, \%seen, $start, $end, $bind);
				if (exists $seen{ $object->as_string }) {
					return Attean::ListIterator->new(
						item_type => 'Attean::API::Result',
						variables => $iter_variables,
						values => [Attean::Result->new()]
					);
				} else {
					return Attean::ListIterator->new(
						item_type => 'Attean::API::Result',
						variables => $iter_variables,
						values => []
					);
				}
			};
		}
	}
}

package Attean::Plan::ZeroOrOnePath 0.037 {
	use Moo;
	use Attean::TreeRewriter;
	use Types::Standard qw(ArrayRef ConsumerOf);
	use namespace::clean;

	has 'subject'		=> (is => 'ro', required => 1);
	has 'object'		=> (is => 'ro', required => 1);
	has 'graph'			=> (is => 'ro', required => 1);
	
	with 'Attean::API::BindingSubstitutionPlan', 'Attean::API::NullaryQueryTree';

	sub BUILDARGS {
		my $class		= shift;
		my %args		= @_;
		my @vars		= map { $_->value } grep { $_->does('Attean::API::Variable') } (@args{qw(subject object)});
		
		if (exists $args{in_scope_variables}) {
			Carp::confess "in_scope_variables is computed automatically, and must not be specified in the $class constructor";
		}
		$args{in_scope_variables}	= \@vars;

		return $class->SUPER::BUILDARGS(%args);
	}
	
	sub tree_attributes { return qw(subject object) };
	with 'Attean::API::Plan', 'Attean::API::UnaryQueryTree';
	
	sub plan_as_string { return 'ZeroOrOnePath' }
	
	sub substitute_impl {
		my $self	= shift;
		my $model	= shift;
		my $bind	= shift;
		my ($impl)	= map { $_->substitute_impl($model, $bind) } @{ $self->children };
		my $iter_variables	= $self->in_scope_variables;

		my $subject	= $self->subject;
		my $object	= $self->object;
		my $graph	= $self->graph;
		for ($subject, $object) {
			if ($_->does('Attean::API::Variable')) {
				my $name	= $_->value;
				if (my $node = $bind->value($name)) {
					$_	= $node;
				}
			}
		}

		my $s_var	= $subject->does('Attean::API::Variable');
		my $o_var	= $object->does('Attean::API::Variable');
		return sub {
			my @extra;
			if ($s_var and $o_var) {
				my $nodes	= $model->graph_nodes($graph);
				while (my $n = $nodes->next) {
					push(@extra, Attean::Result->new( bindings => { map { $_->value => $n } ($subject, $object) } ));
				}
			} elsif ($s_var) {
				push(@extra, Attean::Result->new( bindings => { $subject->value => $object } ));
			} elsif ($o_var) {
				push(@extra, Attean::Result->new( bindings => { $object->value => $subject } ));
			} else {
				if (0 == $subject->compare($object)) {
					push(@extra, Attean::Result->new( bindings => {} ));
				}
			}
			my $iter	= $impl->();
			my %seen;
			return Attean::CodeIterator->new(
				item_type => 'Attean::API::Result',
				variables => $iter_variables,
				generator => sub {
					while (scalar(@extra)) {
						my $r	= shift(@extra);
						unless ($seen{$r->as_string}++) {
							return $r;
						}
					}
					while (my $r = $iter->next()) {
						return unless ($r);
						if ($seen{$r->as_string}++) {
							next;
						}
						return $r;
					}
				}
			);
		};
	}
}

=item * L<Attean::Plan::Exists>

Returns an iterator containing a single boolean term indicating whether any
results were produced by evaluating the sub-plan.

=cut

package Attean::Plan::Exists 0.037 {
	use Moo;
	use Types::Standard qw(ArrayRef ConsumerOf);
	use namespace::clean;
	
	with 'Attean::API::Plan', 'Attean::API::UnaryQueryTree';
	with 'Attean::API::UnionScopeVariablesPlan';

	has variables => (is => 'ro', isa => ArrayRef[ConsumerOf['Attean::API::Variable']]);
	has rows => (is => 'ro', isa => ArrayRef[ConsumerOf['Attean::API::Result']]);

	sub tree_attributes { return qw(variables rows) };
	sub plan_as_string { return 'Exists' }
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my ($impl)	= map { $_->impl($model) } @{ $self->children };
		return sub {
			my $iter	= $impl->();
			my $result	= $iter->next;
# 			if ($result) {
# 				warn "EXISTS: " . $result->as_string;
# 			}
			my $term	= $result ? Attean::Literal->true : Attean::Literal->false;
			return Attean::ListIterator->new(values => [$term], item_type => 'Attean::API::Term');
		}
	}
}

=item * L<Attean::Plan::Aggregate>

=cut

package Attean::Plan::Aggregate 0.037 {
	use Moo;
	use Encode;
	use UUID::Tiny ':std';
	use URI::Escape;
	use I18N::LangTags;
	use POSIX qw(ceil floor);
	use Digest::SHA;
	use Digest::MD5 qw(md5_hex);
	use Scalar::Util qw(blessed);
	use List::Util qw(uniq);
	use Types::Standard qw(ConsumerOf InstanceOf HashRef ArrayRef);
	use namespace::clean;

	with 'Attean::API::Plan', 'Attean::API::UnaryQueryTree';
	has 'aggregates' => (is => 'ro', isa => HashRef[ConsumerOf['Attean::API::Expression']], required => 1);
	has 'groups' => (is => 'ro', isa => ArrayRef[ConsumerOf['Attean::API::Expression']], required => 1);
	has 'active_graphs' => (is => 'ro', isa => ArrayRef[ConsumerOf['Attean::API::IRI']], required => 1);
	
	sub plan_as_string {
		my $self	= shift;
		my @astrings	= map { sprintf('?%s ← %s', $_, $self->aggregates->{$_}->as_string) } keys %{ $self->aggregates };
		my @gstrings	= map { sprintf('%s', $_->as_string) } @{ $self->groups };
		return sprintf('Aggregate { %s } Groups { %s }', join(', ', @astrings), join(', ', @gstrings));
	}
	sub tree_attributes { return qw(aggregates groups) };
	
	sub BUILD {
		# Ensure that the CT extensions are registered
		AtteanX::Functions::CompositeLists->register();
		AtteanX::Functions::CompositeMaps->register();
	}

	sub BUILDARGS {
		my $class		= shift;
		my %args		= @_;
		my $aggs		= $args{ aggregates };
		my @vars		= map { $_->value } grep { $_->does('Attean::API::Variable') } map { $_->value } @{ $args{groups} // [] };
		my @evars		= (@vars, keys %$aggs);
		if (exists $args{in_scope_variables}) {
			Carp::confess "in_scope_variables is computed automatically, and must not be specified in the $class constructor";
		}
		$args{in_scope_variables}	= [@evars];
		return $class->SUPER::BUILDARGS(%args);
	}
	
	sub evaluate_aggregate {
		my $self	= shift;
		my $model	= shift;
		my $expr	= shift;
		my $rows	= shift;
		my $op		= $expr->operator;
		my ($e)		= @{ $expr->children };
# 		my @children	= map { Attean::Plan::Extend->evaluate_expression($model, $_, $r) } @{ $expr->children };
# 		warn "$op — " . join(' ', map { $_->as_string } @children);
		if ($op eq 'COUNT') {
			my $count	= 0;
			foreach my $r (@$rows) {
				if ($e) {
					my $term	= Attean::Plan::Extend->evaluate_expression($model, $e, $r);
					if ($term) {
						$count++;
					}
				} else {
					# This is the special-case branch for COUNT(*)
					$count++;
				}
			}
			return Attean::Literal->new(value => $count, datatype => 'http://www.w3.org/2001/XMLSchema#integer');
		} elsif ($op eq 'SUM') {
			my @cmp;
			my @terms;
			foreach my $r (@$rows) {
				my $term	= Attean::Plan::Extend->evaluate_expression($model, $e, $r);
				if ($term->does('Attean::API::NumericLiteral')) {
					push(@terms, $term);
				}
			}
			my $lhs	= shift(@terms);
			while (my $rhs = shift(@terms)) {
				my $type	= $lhs->binary_promotion_type($rhs, '+');
				my ($lv, $rv)	= map { $_->numeric_value } ($lhs, $rhs);
				$lhs	= Attean::Literal->new(value => ($lv + $rv), datatype => $type);
			}
			return $lhs;
		} elsif ($op eq 'AVG') {
			my @cmp;
			my $count	= 0;
			my $all_ints	= 1;
			my @terms;
			if (scalar(@$rows) == 0) {
				return Attean::Literal->integer(0);
			}
			foreach my $r (@$rows) {
				my $term	= Attean::Plan::Extend->evaluate_expression($model, $e, $r);
				die unless ($term->does('Attean::API::NumericLiteral'));
				push(@terms, $term);
				$count++;
			}
			my $lhs	= shift(@terms);
			while (my $rhs = shift(@terms)) {
				my $type	= $lhs->binary_promotion_type($rhs, '+');
				my ($lv, $rv)	= map { $_->numeric_value } ($lhs, $rhs);
				$lhs	= Attean::Literal->new(value => ($lv + $rv), datatype => $type);
			}
			
			my $rhs			= Attean::Literal->new(value => $count, datatype => 'http://www.w3.org/2001/XMLSchema#integer');
			my ($lv, $rv)	= map { $_->numeric_value } ($lhs, $rhs);
			my $type	= $lhs->binary_promotion_type($rhs, '/');
			return Attean::Literal->new(value => ($lv / $rv), datatype => $type);
		} elsif ($op eq 'SAMPLE') {
			foreach my $r (@$rows) {
				my $term	= Attean::Plan::Extend->evaluate_expression($model, $e, $r);
				return $term if (blessed($term));
			}
		} elsif ($op =~ /^(MIN|MAX)$/) {
			my @cmp;
			foreach my $r (@$rows) {
				my $term	= Attean::Plan::Extend->evaluate_expression($model, $e, $r);
				push(@cmp, $term);
			}
			@cmp	= sort { $a->compare($b) } @cmp;
			return ($op eq 'MIN') ? shift(@cmp) : pop(@cmp);
		} elsif ($op eq 'GROUP_CONCAT') {
			my $sep	= $expr->scalar_vars->{seperator} // ' ';
			my @values;
			my $all_lang	= 1;
			my $all_str		= 1;
			my $lang;
			foreach my $r (@$rows) {
				my $term	= Attean::Plan::Extend->evaluate_expression($model, $e, $r);
				die "GROUP_CONCAT called with a non-literal argument" unless ($term->does('Attean::API::Literal'));
				if ($term->language) {
					$all_str	= 0;
					if (defined($lang) and $lang ne $term->language) {
						$all_lang	= 0;
					} else {
						$lang	= $term->language;
					}
				} else {
					$all_lang	= 0;
					$all_str	= 0;
				}
				push(@values, $term->value);
			}
			my %strtype;
			if ($all_lang and $lang) {
				$strtype{language}	= $lang;
			} elsif ($all_str) {
				$strtype{datatype}	= 'http://www.w3.org/2001/XMLSchema#string'
			}
			my $string	= join($sep, @values);
			return Attean::Literal->new(value => $string, %strtype);
		} elsif ($op eq 'FOLD') {
			my @arg_exprs	= @{ $expr->children };
			my $order	= $expr->order || [];
			my @cmps	= @$order;
			my @exprs	= map { $_->[1] } @cmps;
			my @dirs	= map { $_->[0] eq 'ASC' } @cmps;
			my $l = eval {
				if (scalar(@$order)) {
					# sort $rows by the order condition
					my $fold_cmp	= sub {
						my ($ar, $avalues)	= @$a;
						my ($br, $bvalues)	= @$b;

						my $c	= 0;
						foreach my $i (0 .. $#cmps) {
							my ($av, $bv)	= map { $_->[$i] } ($avalues, $bvalues);
							# Mirrors code in Attean::Plan::OrderBy->sort_rows
							if (not(blessed($av))) {
								$c = -1;
							} elsif (not(blessed($av))) {
								$c = 1;
							} elsif (blessed($av) and $av->does('Attean::API::Binding') and (not(defined($bv)) or not($bv->does('Attean::API::Binding')))) {
								$c	= 1;
							} elsif (blessed($bv) and $bv->does('Attean::API::Binding') and (not(defined($av)) or not($av->does('Attean::API::Binding')))) {
								$c	= -1;
							} else {
								$c		= eval { $av ? $av->compare($bv) : 1 };
								if ($@) {
									$c	= 1;
								}
							}
							$c		*= -1 if ($dirs[$i] == 0);
							last unless ($c == 0);
						}
						$c
					};
					
					my @sorted	= map { $_->[0] } sort { $fold_cmp->() }
									map { my $r = $_; [$r, [map { eval { Attean::Plan::Extend->evaluate_expression( $model, $_, $r ) } } @exprs]] }
									@$rows;

					$rows	= \@sorted;
				}
				
				my %seen;
				if (scalar(@arg_exprs) > 1) {
					# map
					my @values;
					foreach my $r (@$rows) {
						my ($key, $value)	= map { eval { Attean::Plan::Extend->evaluate_expression($model, $_, $r) } || undef } @arg_exprs;
						if (defined($key)) {
							push(@values, $key, $value);
						}
					}
					my $func	= Attean->get_global_functional_form($AtteanX::Functions::CompositeMaps::MAP_TYPE_IRI);
					$func->(undef, undef, @values);
				} else {
					my @values;
					foreach my $r (@$rows) {
						my $term	= eval { Attean::Plan::Extend->evaluate_expression($model, $e, $r) };
						if ($expr->distinct and blessed($term)) {
							next if ($seen{ $term->as_string }++);
						}
						push(@values, $term);
					}
					my $func	= Attean->get_global_functional_form($AtteanX::Functions::CompositeLists::LIST_TYPE_IRI);
					$func->(undef, undef, @values);
				}
			};
			warn $@ if $@;
			return $l;
		} elsif ($op eq 'CUSTOM') {
			my $iri	= $expr->custom_iri;
			my $data	= Attean->get_global_aggregate($iri);
			unless ($data) {
				die "No extension aggregate registered for <$iri>";
			}
			my $start	= $data->{'start'};
			my $process	= $data->{'process'};
			my $finalize	= $data->{'finalize'};

			my $thunk	= $start->($model, $self->active_graphs);
			foreach my $r (@$rows) {
				my $t	= Attean::Plan::Extend->evaluate_expression($model, $e, $r);
				$process->($thunk, $t);
			}
			return $finalize->($thunk);
		} else {
			warn "Unexpected aggregate expression: $op";
		}
		die "$op not implemented";
	}
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my %aggs	= %{ $self->aggregates };
		my @groups	= @{ $self->groups };
		my $iter_variables	= $self->in_scope_variables;

		my $group_template_generator	= sub {
			my $r	= shift;
			my %components;
			foreach my $g (@groups) {
				if ($g->isa('Attean::ValueExpression')) {
					my $value	= $g->value;
					if ($value->isa('Attean::Variable')) {
						my $var	= $value->value;
						my $value	= eval { Attean::Plan::Extend->evaluate_expression($model, $g, $r) };
						if (blessed($value)) {
							$components{$var}	= $value;
						}
					}
				}
			}
			return %components;
		};
		my $group_key_generator	= sub {
			my $r	= shift;
			my @components;
			foreach my $g (@groups) {
				my $value	= eval { Attean::Plan::Extend->evaluate_expression($model, $g, $r) };
				my $key		= blessed($value) ? $value->as_string : '';
				push(@components, $key);
			}
			my $group	= join('|', @components);
			return $group;
		};
		
		my $rank;
		while (my($var, $agg) = each(%aggs)) {
			if ($agg->operator eq 'RANK') {
				$rank	= $var;
			}
		}
		
		my ($impl)	= map { $_->impl($model) } @{ $self->children };
		my %row_groups;
		my %group_templates;
		return sub {
			my $iter	= $impl->();
			while (my $r = $iter->next) {
				my $group_key	= $group_key_generator->($r);
				push(@{ $row_groups{ $group_key } }, $r);
				unless (exists $group_templates{ $group_key }) {
					$group_templates{ $group_key }	= { $group_template_generator->($r) };
				}
			}
			my @group_keys	= keys %row_groups;
			
			# SPARQL evaluation of aggregates over an empty input sequence should
			# result in an empty result <http://answers.semanticweb.com/questions/17410/semantics-of-sparql-aggregates>
			
			my @results;
			if (scalar(@group_keys) == 0 and scalar(@groups) == 0) {
				push(@group_keys, '');
				$row_groups{''}			= [];
				$group_templates{''}	= {};
			}
			foreach my $group (@group_keys) {
				my %row	= %{ $group_templates{ $group } };
				my $rows	= $row_groups{$group};
				if (defined $rank) {
					my $agg			= $aggs{$rank};
					my $ascending	= $agg->scalar_vars->{ascending} // {};
					my $vars	= [map { $_->value->value } @{ $agg->children }];
					# TODO: support ordering by complex expressions in $vars, not just ValueExpressions with variables
					my @sorted	= Attean::Plan::OrderBy->sort_rows($vars, $ascending, $rows);
					my $ord		= 0;
					foreach my $row (@sorted) {
						my %b	= %{ $row->bindings };
						$b{ $rank }	= Attean::Literal->integer($ord++);
						my $r	= Attean::Result->new( bindings => \%b );
						push(@results, $r);
					}
				} else {
					foreach my $var (keys %aggs) {
						my $expr	= $aggs{$var};
						my $value	= eval { $self->evaluate_aggregate($model, $expr, $rows) };
						if ($value) {
							$row{$var}	= $value;
						}
					}
					my $result	= Attean::Result->new( bindings => \%row );
					push(@results, $result);
				}
			}
			return Attean::ListIterator->new(
				values => \@results,
				variables => $iter_variables,
				item_type => 'Attean::API::Result'
			);
		};
	}
}

package Attean::Plan::Sequence 0.037 {
	use Moo;
	use Scalar::Util qw(blessed);
	use Types::Standard qw(ConsumerOf ArrayRef);
	use namespace::clean;
	
	with 'Attean::API::Plan', 'Attean::API::QueryTree';
	with 'Attean::API::UnionScopeVariablesPlan';

	sub plan_as_string { return 'Sequence'; }
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my @children	= map { $_->impl($model) } @{ $self->children };
		return sub {
			foreach my $child (@children) {
				my $iter	= $child->();
				$iter->elements;
			}
			return Attean::ListIterator->new(values => [Attean::Literal->true], item_type => 'Attean::API::Term');
		};
	}
}

package Attean::Plan::Clear 0.037 {
	use Moo;
	use Scalar::Util qw(blessed);
	use Types::Standard qw(ConsumerOf ArrayRef);
	use namespace::clean;
	
	with 'Attean::API::Plan', 'Attean::API::NullaryQueryTree';
	with 'Attean::API::UnionScopeVariablesPlan';

	has 'graphs' => (is => 'ro', isa => ArrayRef[ConsumerOf['Attean::API::Term']]);

	sub plan_as_string {
		my $self	= shift;
		my $level	= shift;
		my $indent	= '  ' x (1+$level);
		my $s		= sprintf("Clear { %d graphs }", scalar(@{ $self->graphs }));
		foreach my $g (@{ $self->graphs }) {
			my $name	= $g->as_sparql;
			chomp($name);
			$s	.= "\n-${indent} $name";
		}
		return $s;
	}
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my $graphs	= $self->graphs;
		return sub {
			foreach my $g (@$graphs) {
				$model->clear_graph($g);
			}
			return Attean::ListIterator->new(values => [Attean::Literal->true], item_type => 'Attean::API::Term');
		};
	}
}

package Attean::Plan::Drop 0.037 {
	use Moo;
	use Scalar::Util qw(blessed);
	use Types::Standard qw(ConsumerOf ArrayRef);
	use namespace::clean;
	
	with 'Attean::API::Plan', 'Attean::API::NullaryQueryTree';
	with 'Attean::API::UnionScopeVariablesPlan';

	has 'graphs' => (is => 'ro', isa => ArrayRef[ConsumerOf['Attean::API::Term']]);

	sub plan_as_string {
		my $self	= shift;
		my $level	= shift;
		my $indent	= '  ' x (1+$level);
		my $s		= sprintf("Drop { %d graphs }", scalar(@{ $self->graphs }));
		foreach my $g (@{ $self->graphs }) {
			$s	.= "\n-${indent} " . $g->as_sparql;
		}
		return $s;
	}
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my $graphs	= $self->graphs;
		return sub {
			foreach my $g (@$graphs) {
				$model->drop_graph($g);
			}
			return Attean::ListIterator->new(values => [Attean::Literal->true], item_type => 'Attean::API::Term');
		};
	}
}

package Attean::Plan::TripleTemplateToModelQuadMethod 0.037 {
	use Moo;
	use Scalar::Util qw(blessed);
	use Types::Standard qw(ConsumerOf Str ArrayRef HashRef);
	use namespace::clean;
	
	with 'Attean::API::Plan', 'Attean::API::UnaryQueryTree';
	with 'Attean::API::UnionScopeVariablesPlan';

	has 'order' => (is => 'ro', isa => ArrayRef[Str], required => 1);
	has 'patterns' => (is => 'ro', isa => HashRef[ArrayRef[ConsumerOf['Attean::API::TripleOrQuadPattern']]], required => 1);
	has 'graph' => (is => 'ro', isa => ConsumerOf['Attean::API::Term']);

	sub plan_as_string {
		my $self	= shift;
		my $level	= shift;
		my $indent	= '  ' x (1+$level);
		my $s		= sprintf("Template-to-Model { Default graph: %s }", $self->graph->as_string);
		foreach my $method (@{ $self->order }) {
			my $pattern	= $self->patterns->{ $method };
			$s	.= "\n-${indent} Method: ${method}";
			foreach my $p (@$pattern) {
				$s	.= "\n-${indent}   " . $p->as_string;
			}
		}
		return $s;
	}
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my $child	= $self->children->[0]->impl($model);
		
		my $graph	= $self->graph;
		my @order	= @{ $self->order };
		my $method	= shift(@order);
		my $pattern	= $self->patterns->{ $method };

		return sub {
			my $iter	= $child->();
			my @results;
			while (my $t = $iter->next) {
				if (scalar(@order)) {
					push(@results, $t);
				}
				foreach my $p (@$pattern) {
					my $q		= $p->apply_bindings($t);
					my $quad	= $q->does('Attean::API::QuadPattern') ? $q : $q->as_quad_pattern($graph);
					if ($quad->is_ground) {
						# warn "# $method: " . $quad->as_string . "\n";
						$model->$method($quad->as_quad);
					} else {
						# warn "not ground: " . $quad->as_string;
					}
				}
			}
			foreach my $method (@order) {
				my $pattern	= $self->patterns->{ $method };
				foreach my $t (@results) {
					foreach my $p (@$pattern) {
						my $q		= $p->apply_bindings($t);
						my $quad	= $q->does('Attean::API::QuadPattern') ? $q : $q->as_quad_pattern($graph);
						if ($quad->is_ground) {
							# warn "# $method: " . $quad->as_string . "\n";
							$model->$method($quad->as_quad);
						} else {
							# warn "not ground: " . $quad->as_string;
						}
					}
				}
			}
			return Attean::ListIterator->new(values => [Attean::Literal->integer($model->size)], item_type => 'Attean::API::Term');
		};
	}
}

package Attean::Plan::Load 0.037 {
	use Moo;
	use Encode;
	use LWP::UserAgent;
	use Scalar::Util qw(blessed);
	use Types::Standard qw(Bool Str);
	use namespace::clean;
	
	with 'Attean::API::Plan', 'Attean::API::NullaryQueryTree';
	with 'Attean::API::UnionScopeVariablesPlan';

	has 'silent' => (is => 'ro', isa => Bool, default => 0);
	has 'url' => (is => 'ro', isa => Str);

	sub plan_as_string {
		my $self	= shift;
		return sprintf("Load { %s }", $self->url);
	}
	
	sub impl {
		my $self	= shift;
		my $url		= $self->url;
		my $ua		= LWP::UserAgent->new();
		my $silent	= $self->silent;
		my $accept	= Attean->acceptable_parsers( handles => 'Attean::API::Triple' );
		$ua->default_headers->push_header( 'Accept' => $accept );
		return sub {
			my $resp	= $ua->get( $url );
			if ($resp->is_success) {
				my $ct		= $resp->header('Content-Type');
				if (my $pclass = Attean->get_parser( media_type => $ct )) {
					my $p		= $pclass->new();
					my $str		= $resp->decoded_content;
					my $bytes	= encode('UTF-8', $str, Encode::FB_CROAK);
					my $iter	= $p->parse_iter_from_bytes( $bytes );
					return $iter;
				}
			}
			
			if ($silent) {
				return Attean::ListIterator->new(values => [], item_type => 'Attean::API::Triple');
			} else {
				die "Failed to load url: " . $resp->status_line;
			}
		};
	}
}



=item * L<Attean::Plan::Unfold>

=cut

package Attean::Plan::Unfold 0.032 {
	use Moo;
	use Encode;
	use UUID::Tiny ':std';
	use URI::Escape;
	use Data::Dumper;
	use I18N::LangTags;
	use POSIX qw(ceil floor);
	use Digest::SHA;
	use Digest::MD5 qw(md5_hex);
	use Scalar::Util qw(blessed looks_like_number);
	use List::Util qw(uniq all);
	use Types::Standard qw(ConsumerOf ArrayRef InstanceOf HashRef);
	use namespace::clean;

	with 'MooX::Log::Any';
	with 'Attean::API::BindingSubstitutionPlan', 'Attean::API::UnaryQueryTree';
	
	has 'variables' => (is => 'ro', isa => ArrayRef[ConsumerOf['Attean::API::Variable']], required => 1);
	has 'expression' => (is => 'ro', isa => ConsumerOf['Attean::API::Expression'], required => 1);
	has 'active_graphs' => (is => 'ro', isa => ArrayRef[ConsumerOf['Attean::API::IRI']], required => 1);
	
	sub plan_as_string {
		my $self	= shift;
		my @vars	= map { $_->as_string } @{ $self->variables };
		my $vars	= '(' . join(', ', @vars) . ')';
		return sprintf('Unfold { %s ← %s }', $vars, $self->expression->as_string);
	}
	sub tree_attributes { return qw(variable expression) };
	
	sub BUILDARGS {
		my $class		= shift;
		my %args		= @_;
		my $exprs		= $args{ expressions };
		my @vars		= map { @{ $_->in_scope_variables } } @{ $args{ children } };
		my @evars		= (@vars, keys %$exprs);
		
		if (exists $args{in_scope_variables}) {
			Carp::confess "in_scope_variables is computed automatically, and must not be specified in the $class constructor";
		}
		$args{in_scope_variables}	= [@evars];
		return $class->SUPER::BUILDARGS(%args);
	}
	
	sub substitute_impl {
		my $self	= shift;
		my $model	= shift;
		my $bind	= shift;
		my $expr	= $self->expression;
		my $vars	= $self->variables;
		my ($impl)	= map { $_->substitute_impl($model, $bind) } @{ $self->children };
		# TODO: substitute variables in the expression
		return $self->_impl($model, $impl, $expr, @$vars);
	}
	
	sub impl {
		my $self	= shift;
		my $model	= shift;
		my $expr	= $self->expression;
		my $vars	= $self->variables;
		my ($impl)	= map { $_->impl($model) } @{ $self->children };
		return $self->_impl($model, $impl, $expr, @$vars);
	}
	
	sub _impl {
		my $self	= shift;
		my $model	= shift;
		my $impl	= shift;
		Carp::confess unless (defined($impl));
		my $expr	= shift;
		my @vars	= @_;
		my $iter_variables	= $self->in_scope_variables;

		my $var			= $vars[0];
		my $index_var	= $vars[1];
		return sub {
			my $iter	= $impl->();
			my @buffer;
			return Attean::CodeIterator->new(
				item_type => 'Attean::API::Result',
				variables => $iter_variables,
				generator => sub {
					if (scalar(@buffer)) {
						return shift(@buffer);
					}
					ROW: while (my $r = $iter->next) {
						my %base	= map { $_ => $r->value($_) } $r->variables;
						my $cdt	= eval { Attean::Plan::Extend->evaluate_expression($model, $expr, $r) };
						warn "UNFOLD expression evaluation error: $@" if ($@);
						unless ($cdt) {
							return $r;
						}
						
						if ($cdt->does('Attean::API::Literal') and $cdt->datatype->value eq $AtteanX::Functions::CompositeLists::LIST_TYPE_IRI) {
							my @values	= AtteanX::Functions::CompositeLists::lex_to_list($cdt);
							foreach my $index (0 .. $#values) {
								my %row	= %base;
								my $term	= $values[$index];
								if (blessed($term)) {
									if ($row{ $var } and $term->as_string ne $row{ $var }->as_string) {
										next ROW;
									}
								
									if ($term->does('Attean::API::Binding')) {
										# patterns need to be made ground to be bound as values (e.g. TriplePattern -> Triple)
										$term	= $term->ground($r);
									}
								
									$row{ $var->value }	= $term;
									if (defined $index_var) {
										$row{ $index_var->value }	= Attean::Literal->integer(1+$index);
									}
								}
								push(@buffer, Attean::Result->new( bindings => \%row, eval_stash => $r->eval_stash ));
							}
						} elsif ($cdt->does('Attean::API::Literal') and $cdt->datatype->value eq $AtteanX::Functions::CompositeMaps::MAP_TYPE_IRI) {
							my @values	= AtteanX::Functions::CompositeMaps::lex_to_maplist($cdt);
							while (my @pair = splice(@values, 0, 2)) {
								my ($index, $term)	= @pair;
								my %row	= %base;
								if (blessed($term)) {
									if ($row{ $var } and $term->as_string ne $row{ $var }->as_string) {
										next ROW;
									}
								
									if ($term->does('Attean::API::Binding')) {
										# patterns need to be made ground to be bound as values (e.g. TriplePattern -> Triple)
										$term	= $term->ground($r);
									}
								
									if (defined $index_var) {
										# 2-var
										$row{ $index_var->value }	= $term;
										$row{ $var->value }	= $index;
									} else {
										# 1-var
										$row{ $var->value }	= $term;
									}
								}
								push(@buffer, Attean::Result->new( bindings => \%row, eval_stash => $r->eval_stash ));
							}
						}
						return shift(@buffer);
					}
					return;
				}
			);
		};
	}
}
# Create(iri)

1;

__END__

=back

=head1 BUGS

Please report any bugs or feature requests to through the GitHub web interface
at L<https://github.com/kasei/attean/issues>.

=head1 SEE ALSO



=head1 AUTHOR

Gregory Todd Williams  C<< <gwilliams@cpan.org> >>

=head1 COPYRIGHT

Copyright (c) 2014--2022 Gregory Todd Williams.
This program is free software; you can redistribute it and/or modify it under
the same terms as Perl itself.

=cut
