From 7633a668bc6c6760d748e5f0003978fcd1eccf43 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Sun, 17 Mar 2024 15:48:59 -0400 Subject: [PATCH 1/5] MathObject version of the new Boolean context --- macros/contexts/contextBoolean.pl | 319 ++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 macros/contexts/contextBoolean.pl diff --git a/macros/contexts/contextBoolean.pl b/macros/contexts/contextBoolean.pl new file mode 100644 index 0000000000..a79ec22367 --- /dev/null +++ b/macros/contexts/contextBoolean.pl @@ -0,0 +1,319 @@ +## contextBoolean.pl + +sub _contextBoolean_init { context::Boolean::Init() } + +package context::Boolean; + +sub Init { + my $context = $main::context{Boolean} = Parser::Context->getCopy('Numeric'); + $context->{name} = 'Boolean'; + + $context->{parser}{Formula} = 'context::Boolean::Formula'; + $context->{value}{Formula} = 'context::Boolean::Formula'; + $context->{value}{Boolean} = 'context::Boolean::Boolean'; + $context->{value}{Real} = 'context::Boolean::Boolean'; + $context->{precedence}{Boolean} = $context->{precedence}{Real}; + + ## Disable unnecessary context stuff + $context->functions->disable('All'); + $context->strings->clear(); + $context->lists->clear(); + + ## Define our logic operators + # (for now...) + # all binary operators have the same precedence and process left-to-right + # any parens to the right must be preserved with consecutive binary ops + $context->operators->are( + 'or' => { + class => 'context::Boolean::BOP::or', + precedence => 1, + associativity => 'left', + type => 'bin', + rightparens => 'same', + string => ' or ', + TeX => '\vee ', + perl => '||', + alternatives => ["\x{2228}"], + }, + 'and' => { + class => 'context::Boolean::BOP::and', + precedence => 1, + associativity => 'left', + type => 'bin', + rightparens => 'same', + string => ' and ', + TeX => '\wedge ', + perl => '&&', + alternatives => ["\x{2227}"], + }, + 'xor' => { + class => 'context::Boolean::BOP::xor', + precedence => 1, + associativity => 'left', + type => 'bin', + rightparens => 'same', + string => ' xor ', + perl => '!=', + TeX => '\oplus ', + alternatives => [ "\x{22BB}", "\x{2295}" ], + }, + 'not' => { + class => 'context::Boolean::UOP::not', + precedence => 3, + associativity => 'left', + type => 'unary', + string => 'not ', + TeX => '\mathord{\sim}', + perl => '!', + alternatives => ["\x{00AC}"], + }, + ' ' => { + class => 1, + precedence => 1, + associativity => 'left', + type => 'bin', + string => 'and', + hidden => 1 + }, + '*' => { alias => 'and' }, + '+' => { alias => 'or' }, + '-' => { alias => 'not' }, + '!' => { alias => 'not' }, + '~' => { alias => 'not', alternatives => ["\x{223C}"] }, + '><' => { alias => 'xor' }, + ); + + ## redefine, but disable some usual context tokens for 'clearer' error messages + $context->operators->redefine([ ',', 'fn' ], from => 'Numeric'); + $context->lists->redefine('List', from => 'Numeric'); + $context->operators->redefine([ '/', '^', '**' ], from => 'Numeric'); + $context->operators->undefine('/', '^', '**'); + delete $context->operators->get('/')->{space}; + + ## Set default variables 'p' and 'q' + $Parser::Context::Variables::type{Boolean} = $Parser::Context::Variables::type{Real}; + $context->variables->are( + p => 'Boolean', + q => 'Boolean', + ); + + ## Set up new reduction rules: + $context->reductions->set('x||1' => 1, 'x||0' => 1, 'x&&1' => 1, 'x&&0' => 1, '!!x' => 1); + + ## Value::inContext does not recognize our $context as second argument -- using inContext method instead + our $T = context::Boolean::Boolean->new(1)->inContext($context); + our $F = context::Boolean::Boolean->new(0)->inContext($context); + + ## Define constants for 'True' and 'False' + $context->constants->are( + 'T' => { value => $T, TeX => '\top ', perl => '$context::Boolean::T' }, #, alternatives => ["\x{22A4}"] }, + 'F' => { value => $F, TeX => '\bot ', perl => '$context::Boolean::F' }, #, alternatives => ["\x{22A5}"] }, + 'True' => { alias => 'T' }, + 'False' => { alias => 'F' }, + ); + + ## allow authors to create Boolean values + main::PG_restricted_eval('sub Boolean { Value->Package("Boolean()")->new(@_) }'); +} + +## Subclass Value::Formula for boolean formulas +package context::Boolean::Formula; +our @ISA = ('Value::Formula'); + +## use every combination of T/F across all variables +sub createRandomPoints { + my $self = shift; + my $context = $self->{context}; + my @variables = $context->variables->names; + my @points; + my @values; + + my $T = $context::Boolean::T->inContext($context); + my $F = $context::Boolean::F->inContext($context); + + my $f = $self->{f}; + $f = $self->{f} = $self->perlFunction(undef, \@variables) unless $f; + + foreach my $combination (0 .. 2**@variables - 1) { + my @point = map { $combination & 2**$_ ? $T : $F } (0 .. $#variables); + my $value = &$f(@point); + push @points, \@point; + push @values, $value; + } + + $self->{test_points} = \@points; + $self->{test_values} = \@values; + return \@points; +} + +package context::Boolean::BOP; +our @ISA = qw(Parser::BOP); + +sub _check { + my $self = shift; + return if $self->checkNumbers; + $self->Error("Operands of '%s' must be 'Boolean'", $self->{bop}); +} + +sub perl { + my $self = shift; + my $l = $self->{lop}; + my $r = $self->{rop}; + my $bop = $self->{def}{perl} || $self->{def}{string}; + my $lPerl = $self->{lop}->perl(1) . '->value'; + my $rPerl = $self->{rop}->perl(2) . '->value'; + my $result = "$lPerl $bop $rPerl"; + return "($result ? \$context::Boolean::T : \$context::Boolean::F)"; +} + +package context::Boolean::BOP::or; +our @ISA = qw(context::Boolean::BOP); + +sub _eval { ($_[1]->value || $_[2]->value ? $context::Boolean::T : $context::Boolean::F) } + +sub _reduce { + my $self = shift; + my $context = $self->{equation}{context}; + my $reduce = $context->{reduction}; + my $l = $self->{lop}; + my $r = $self->{rop}; + + return $self unless ($l->{isConstant} || $r->{isConstant}); + + # make sure we are comparing to an updated 'true' + my $T = $context::Boolean::T->inContext($context); + + # using string since Parser::Number::eval is unblessed, while Parser::Constant::eval is a (truthy) MathObject + if ($l->{isConstant}) { + return $l->string eq "$T" ? ($reduce->{'x||1'} ? $l : $self) : ($reduce->{'x||0'} ? $r : $self); + } else { + return $r->string eq "$T" ? ($reduce->{'x||1'} ? $r : $self) : ($reduce->{'x||0'} ? $l : $self); + } +} + +package context::Boolean::BOP::and; +our @ISA = qw(context::Boolean::BOP); + +sub _eval { ($_[1]->value && $_[2]->value ? $context::Boolean::T : $context::Boolean::F) } + +sub _reduce { + my $self = shift; + my $context = $self->{equation}{context}; + my $reduce = $context->{reduction}; + my $l = $self->{lop}; + my $r = $self->{rop}; + + return $self unless ($l->{isConstant} || $r->{isConstant}); + + # make sure we are comparing to an updated 'true' + my $T = $context::Boolean::T->inContext($context); + + # using string since Parser::Number::eval is unblessed, while Parser::Constant::eval is a (truthy) MathObject + if ($l->{isConstant}) { + return $l->string eq "$T" ? ($reduce->{'x&&1'} ? $r : $self) : ($reduce->{'x&&0'} ? $l : $self); + } else { + return $r->string eq "$T" ? ($reduce->{'x&&1'} ? $l : $self) : ($reduce->{'x&&0'} ? $r : $self); + } +} + +package context::Boolean::BOP::xor; +our @ISA = qw(context::Boolean::BOP); + +sub _eval { ($_[1]->value != $_[2]->value ? $context::Boolean::T : $context::Boolean::F) } + +package context::Boolean::UOP::not; +our @ISA = qw(Parser::UOP); + +sub _check { + my $self = shift; + return if $self->checkNumber; + $self->Error("Operands of '%s' must be 'Boolean'", $self->{uop}); +} + +sub _reduce { + my $self = shift; + my $context = $self->{equation}{context}; + my $reduce = $context->{reduction}; + my $op = $self->{op}; + + if ($op->isNeg && $reduce->{'!!x'}) { + delete $op->{op}{noParens}; + return $op->{op}; + } + + if ($op->{isConstant} && $context->flag('reduceConstants')) { + # same issue with Parser::Number::eval being unblessed, check string instead + my $T = $context::Boolean::T->inContext($context); + my $new = $op->string eq "$T" ? 0 : 1; + return $self->Item('Value')->new($self->{equation}, [$new]); + } + return $self; +} + +sub isNeg {1} + +sub _eval { (!($_[1]->value) ? $context::Boolean::T : $context::Boolean::F) } + +sub perl { + my $self = shift; + my $op = $self->{def}{perl} || $self->{def}{string}; + my $perl = $self->{op}->perl(1) . '->value'; + my $result = "$op $perl"; + return "($result ? \$context::Boolean::T : \$context::Boolean::F)"; +} + +package context::Boolean::Boolean; +our @ISA = qw(Value::Real); + +sub new { + my $self = shift; + my $value = $self->SUPER::new(@_); + $value->checkBoolean unless $value->classMatch("Formula"); + return $value; +} + +sub make { + my $self = shift; + my $result = $self->SUPER::make(@_); + $result->checkBoolean unless $result->classMatch("Formula"); + return $result; +} + +sub checkBoolean { + my $self = shift; + $self->Error("Numeric values can only be 1 or 0 in this context") + unless ($self->value == 1 || $self->value == 0); +} + +sub compare { + # copypasta from other compare methods -- is this necessary? + my ($self, $l, $r) = Value::checkOpOrderWithPromote(@_); + return $l->value <=> $r->value; +} + +## use the context settings +sub string { + my $self = shift; + my $const = $self->context->constants; + my $T = $const->get('T')->{string} || 'T'; + my $F = $const->get('F')->{string} || 'F'; + return ($F, $T)[ $self->value ]; +} + +## use the context settings +sub TeX { + my $self = shift; + my $const = $self->context->constants; + my $T = $const->get('T')->{TeX} || '\top'; + my $F = $const->get('F')->{TeX} || '\bot'; + return ($F, $T)[ $self->value ]; +} + +sub perl { + my $self = shift; + return $self->value ? '$context::Boolean::T' : '$context::Boolean::F'; +} + +sub cmp_defaults { shift->SUPER::cmp_defaults(@_) } + +1; From fedc1fea8c3b7911b7411f75cadd947dde570461 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Mon, 18 Mar 2024 19:59:40 -0400 Subject: [PATCH 2/5] incorporate code from @dpvc --- macros/contexts/contextBoolean.pl | 193 +++++++++++++++++++++--------- 1 file changed, 137 insertions(+), 56 deletions(-) diff --git a/macros/contexts/contextBoolean.pl b/macros/contexts/contextBoolean.pl index a79ec22367..57d3db2072 100644 --- a/macros/contexts/contextBoolean.pl +++ b/macros/contexts/contextBoolean.pl @@ -8,6 +8,7 @@ sub Init { my $context = $main::context{Boolean} = Parser::Context->getCopy('Numeric'); $context->{name} = 'Boolean'; + $context->{parser}{Number} = 'context::Boolean::Number'; $context->{parser}{Formula} = 'context::Boolean::Formula'; $context->{value}{Formula} = 'context::Boolean::Formula'; $context->{value}{Boolean} = 'context::Boolean::Boolean'; @@ -26,7 +27,7 @@ sub Init { $context->operators->are( 'or' => { class => 'context::Boolean::BOP::or', - precedence => 1, + precedence => 3, associativity => 'left', type => 'bin', rightparens => 'same', @@ -37,7 +38,7 @@ sub Init { }, 'and' => { class => 'context::Boolean::BOP::and', - precedence => 1, + precedence => 3, associativity => 'left', type => 'bin', rightparens => 'same', @@ -48,7 +49,7 @@ sub Init { }, 'xor' => { class => 'context::Boolean::BOP::xor', - precedence => 1, + precedence => 3, associativity => 'left', type => 'bin', rightparens => 'same', @@ -68,19 +69,21 @@ sub Init { alternatives => ["\x{00AC}"], }, ' ' => { - class => 1, + class => 3, precedence => 1, associativity => 'left', type => 'bin', string => 'and', hidden => 1 }, - '*' => { alias => 'and' }, - '+' => { alias => 'or' }, - '-' => { alias => 'not' }, - '!' => { alias => 'not' }, - '~' => { alias => 'not', alternatives => ["\x{223C}"] }, - '><' => { alias => 'xor' }, + '*' => { alias => 'and' }, + '/\\' => { alias => 'and' }, + '+' => { alias => 'or' }, + '\\/' => { alias => 'or' }, + '-' => { alias => 'not' }, + '!' => { alias => 'not' }, + '~' => { alias => 'not', alternatives => ["\x{223C}"] }, + '><' => { alias => 'xor' }, ); ## redefine, but disable some usual context tokens for 'clearer' error messages @@ -100,22 +103,103 @@ sub Init { ## Set up new reduction rules: $context->reductions->set('x||1' => 1, 'x||0' => 1, 'x&&1' => 1, 'x&&0' => 1, '!!x' => 1); - ## Value::inContext does not recognize our $context as second argument -- using inContext method instead - our $T = context::Boolean::Boolean->new(1)->inContext($context); - our $F = context::Boolean::Boolean->new(0)->inContext($context); - ## Define constants for 'True' and 'False' + $context->constants->{namePattern} = qr/(?:\w|[\x{22A4}\x{22A5}])+/; $context->constants->are( - 'T' => { value => $T, TeX => '\top ', perl => '$context::Boolean::T' }, #, alternatives => ["\x{22A4}"] }, - 'F' => { value => $F, TeX => '\bot ', perl => '$context::Boolean::F' }, #, alternatives => ["\x{22A5}"] }, + T => { + value => context::Boolean::Boolean->new($context, 1), + string => 'T', + TeX => '\top', + perl => 'context::Boolean->T', + isConstant => 1, + alternatives => ["\x{22A4}"] + }, + F => { + value => context::Boolean::Boolean->new($context, 0), + string => 'F', + TeX => '\bot', + perl => 'context::Boolean->F', + isConstant => 1, + alternatives => ["\x{22A5}"] + }, 'True' => { alias => 'T' }, 'False' => { alias => 'F' }, ); + ## add our methods to this context + bless $context, 'context::Boolean::Context'; + ## allow authors to create Boolean values main::PG_restricted_eval('sub Boolean { Value->Package("Boolean()")->new(@_) }'); } +## top-level access to context-specific T and T +sub T { + my $context = main::Context(); + Value::Error("Context must be a Boolean context") unless $context->can('T'); + return $context->T; +} + +sub F { + my $context = main::Context(); + Value::Error("Context must be a Boolean context") unless $context->can('F'); + return $context->F; +} + +## Subclass the Parser::Context to override copy() and add T and F functions +package context::Boolean::Context; +our @ISA = ('Parser::Context'); + +sub copy { + my $self = shift->SUPER::copy(@_); + ## update the T and F constants to refer to this context + $self->constants->set( + T => { value => context::Boolean::Boolean->new($self, 1) }, + F => { value => context::Boolean::Boolean->new($self, 0) } + ); + return $self; +} + +## Access to the constant T and F values +sub F { shift->constants->get('F')->{value} } +sub T { shift->constants->get('T')->{value} } + +## Easy setting of precedence to different types +sub setPrecedence { + my ($self, $order) = @_; + if ($order eq 'equal') { + $self->operators->set( + or => { precedence => 3 }, + xor => { precedence => 3 }, + and => { precedence => 3 }, + not => { precedence => 3 }, + ); + } elsif ($order eq 'oxan') { + $self->operators->set( + or => { precedence => 1 }, + xor => { precedence => 2 }, + and => { precedence => 3 }, + not => { precedence => 6 }, + ); + } else { + Value::Error("Unknown precedence class '%s'", $order); + } +} + +## Subclass Parser::Number to return the constant T or F +package context::Boolean::Number; +our @ISA = ('Parser::Number'); + +sub eval { + my $self = shift; + return $self->context->constants->get(('F', 'T')[ $self->{value} ])->{value}; +} + +sub perl { + my $self = shift; + return $self->context->constants->get(('F', 'T')[ $self->{value} ])->{perl}; +} + ## Subclass Value::Formula for boolean formulas package context::Boolean::Formula; our @ISA = ('Value::Formula'); @@ -128,8 +212,8 @@ sub createRandomPoints { my @points; my @values; - my $T = $context::Boolean::T->inContext($context); - my $F = $context::Boolean::F->inContext($context); + my $T = $context->T; + my $F = $context->F; my $f = $self->{f}; $f = $self->{f} = $self->perlFunction(undef, \@variables) unless $f; @@ -163,63 +247,62 @@ sub perl { my $lPerl = $self->{lop}->perl(1) . '->value'; my $rPerl = $self->{rop}->perl(2) . '->value'; my $result = "$lPerl $bop $rPerl"; - return "($result ? \$context::Boolean::T : \$context::Boolean::F)"; + return "($result ? context::Boolean->T : context::Boolean->F)"; } package context::Boolean::BOP::or; our @ISA = qw(context::Boolean::BOP); -sub _eval { ($_[1]->value || $_[2]->value ? $context::Boolean::T : $context::Boolean::F) } +sub _eval { + my ($self, $l, $r) = @_; + return ($l->value || $r->value ? $self->context->T : $self->context->F); +} sub _reduce { - my $self = shift; - my $context = $self->{equation}{context}; - my $reduce = $context->{reduction}; - my $l = $self->{lop}; - my $r = $self->{rop}; + my $self = shift; + my $reduce = $self->context->{reduction}; + my $l = $self->{lop}; + my $r = $self->{rop}; return $self unless ($l->{isConstant} || $r->{isConstant}); - # make sure we are comparing to an updated 'true' - my $T = $context::Boolean::T->inContext($context); - - # using string since Parser::Number::eval is unblessed, while Parser::Constant::eval is a (truthy) MathObject if ($l->{isConstant}) { - return $l->string eq "$T" ? ($reduce->{'x||1'} ? $l : $self) : ($reduce->{'x||0'} ? $r : $self); + return $l->eval->value ? ($reduce->{'x||1'} ? $l : $self) : ($reduce->{'x||0'} ? $r : $self); } else { - return $r->string eq "$T" ? ($reduce->{'x||1'} ? $r : $self) : ($reduce->{'x||0'} ? $l : $self); + return $r->eval->value ? ($reduce->{'x||1'} ? $r : $self) : ($reduce->{'x||0'} ? $l : $self); } } package context::Boolean::BOP::and; our @ISA = qw(context::Boolean::BOP); -sub _eval { ($_[1]->value && $_[2]->value ? $context::Boolean::T : $context::Boolean::F) } +sub _eval { + my ($self, $l, $r) = @_; + return ($l->value && $r->value ? $self->context->T : $self->context->F); +} sub _reduce { - my $self = shift; - my $context = $self->{equation}{context}; - my $reduce = $context->{reduction}; - my $l = $self->{lop}; - my $r = $self->{rop}; + my $self = shift; + my $reduce = $self->context->{reduction}; + my $l = $self->{lop}; + my $r = $self->{rop}; return $self unless ($l->{isConstant} || $r->{isConstant}); - # make sure we are comparing to an updated 'true' - my $T = $context::Boolean::T->inContext($context); - - # using string since Parser::Number::eval is unblessed, while Parser::Constant::eval is a (truthy) MathObject if ($l->{isConstant}) { - return $l->string eq "$T" ? ($reduce->{'x&&1'} ? $r : $self) : ($reduce->{'x&&0'} ? $l : $self); + return $l->eval->value ? ($reduce->{'x&&1'} ? $r : $self) : ($reduce->{'x&&0'} ? $l : $self); } else { - return $r->string eq "$T" ? ($reduce->{'x&&1'} ? $l : $self) : ($reduce->{'x&&0'} ? $r : $self); + return $r->eval->value ? ($reduce->{'x&&1'} ? $l : $self) : ($reduce->{'x&&0'} ? $r : $self); } } package context::Boolean::BOP::xor; our @ISA = qw(context::Boolean::BOP); -sub _eval { ($_[1]->value != $_[2]->value ? $context::Boolean::T : $context::Boolean::F) } +sub _eval { + my ($self, $l, $r) = @_; + return ($l->value != $r->value ? $self->context->T : $self->context->F); +} package context::Boolean::UOP::not; our @ISA = qw(Parser::UOP); @@ -231,10 +314,9 @@ sub _check { } sub _reduce { - my $self = shift; - my $context = $self->{equation}{context}; - my $reduce = $context->{reduction}; - my $op = $self->{op}; + my $self = shift; + my $reduce = $self->context->{reduction}; + my $op = $self->{op}; if ($op->isNeg && $reduce->{'!!x'}) { delete $op->{op}{noParens}; @@ -242,24 +324,24 @@ sub _reduce { } if ($op->{isConstant} && $context->flag('reduceConstants')) { - # same issue with Parser::Number::eval being unblessed, check string instead - my $T = $context::Boolean::T->inContext($context); - my $new = $op->string eq "$T" ? 0 : 1; - return $self->Item('Value')->new($self->{equation}, [$new]); + return $self->Item('Value')->new($self->{equation}, [ 1 - $op->value ]); } return $self; } sub isNeg {1} -sub _eval { (!($_[1]->value) ? $context::Boolean::T : $context::Boolean::F) } +sub _eval { + my ($self, $op) = @_; + return (!($op->value) ? $self->context->T : $self->context->F); +} sub perl { my $self = shift; my $op = $self->{def}{perl} || $self->{def}{string}; my $perl = $self->{op}->perl(1) . '->value'; my $result = "$op $perl"; - return "($result ? \$context::Boolean::T : \$context::Boolean::F)"; + return "($result ? context::Boolean->T : context::Boolean->F)"; } package context::Boolean::Boolean; @@ -286,7 +368,6 @@ sub checkBoolean { } sub compare { - # copypasta from other compare methods -- is this necessary? my ($self, $l, $r) = Value::checkOpOrderWithPromote(@_); return $l->value <=> $r->value; } @@ -311,7 +392,7 @@ sub TeX { sub perl { my $self = shift; - return $self->value ? '$context::Boolean::T' : '$context::Boolean::F'; + return $self->value ? 'context::Boolean->T' : 'context::Boolean->F'; } sub cmp_defaults { shift->SUPER::cmp_defaults(@_) } From 50c1155d18472d3d8ccebf2d4ba61c86385e545e Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Wed, 1 May 2024 15:28:20 -0400 Subject: [PATCH 3/5] incorporate feedback and add POD --- macros/contexts/contextBoolean.pl | 250 +++++++++++++++++++++++++----- 1 file changed, 211 insertions(+), 39 deletions(-) diff --git a/macros/contexts/contextBoolean.pl b/macros/contexts/contextBoolean.pl index 57d3db2072..5e22f80b6f 100644 --- a/macros/contexts/contextBoolean.pl +++ b/macros/contexts/contextBoolean.pl @@ -1,4 +1,128 @@ -## contextBoolean.pl + +=head1 NAME + +contextBoolean.pl - Implements a MathObject class for Boolean expressions + +=head1 DESCRIPTION + +Load this file: + + loadMacros('contextBoolean.pl'); + +and then select the context: + + Context('Boolean'); + +=head2 CONSTANTS + +This constant recognizes two constants by default, C and C. The following are all equivalent: + + $T = Compute('1'); + $T = Boolean('T'); + $T = Context()->T; + $T = context::Boolean->T; + +=head2 VARIABLES + +By default, this context has two variables, C

and C. More variables can be added through the usual +means of modifying context: + + Context->variables->add( r => 'Boolean' ); + +=head2 OPERATORS + +Changing the LaTeX representations of the boolean operators is handled through the operators C, C, +C, and C. Note the extra space following the LaTeX command. + + Context->operators->set( not => { TeX => '\neg ' } ); + + +=head3 Aliases and Alternatives + +Modifications to the operators should be applied to the string versions of each operator: 'or', 'xor', 'and', +and 'not'; rather than to any of the following aliases or alternatives. + +=over + +=item OR + +The 'or' operator is indicated by C, C<+>, C<\\/>, C, or unicode C. + +=item AND + +The 'and' operator is indicated by C, C<*>, whitespace (as with implicit multiplication), C, C, +or unicode C. + +=item XOR + +The 'xor' operator is indicated by C, C<\>\<>, C, or unicodes C, C. + +=item NOT + +The 'not' operator is indicated by C, C<->, C, C<~>, or unicodes C, C. + +A right-associative version of the 'not' operator is also available by using C<'> or C<`> following the expression +to be negated. + +=back + +=head2 OPERATOR PRECEDENCE + +=over + +=item S>> + +This context supports two paradigms for operation precedence: C (default) and C. + +The default setting, C, gives all boolean operations the same priority, meaning that parenthesis +are the only manner by which an expression will evaluate operations to the right before those to the left. + + $a = Compute("T or T and F"); # $a == F + +The C setting priortizes C < C < C < C. + + Context()->setPrecedence('oxan'); + $b = Compute("T or T and F"); # $b == T + +=back + +=head2 REDUCTION + +The context also handles C with the following reduction rules: + +=over + +=item C<'x||1'> + + $f = Formula('p or T')->reduce; # $f == T + +=item C<'x||0'> + + $f = Formula('p or F')->reduce; # $f == Formula('p') + +=item C<'x&&1'> + + $f = Formula('p and T')->reduce; # $f == Formula('p') + +=item C<'x&&0'> + + $f = Formula('p and F')->reduce; # $f == F + +=item C<'!!x'> + + $f = Formula('not not p')->reduce; # $f == Formula('p'); + +=back + +=head2 COMPARISON + +Boolean Formula objects are considered equal whenever the two expressions generate the same truth table. + + $f = Formula('not (p or q)'); + $g = Formula('(not p) and (not q)'); + # $f == $g is true + +=cut sub _contextBoolean_init { context::Boolean::Init() } @@ -15,15 +139,12 @@ sub Init { $context->{value}{Real} = 'context::Boolean::Boolean'; $context->{precedence}{Boolean} = $context->{precedence}{Real}; - ## Disable unnecessary context stuff + # Disable unnecessary context stuff $context->functions->disable('All'); $context->strings->clear(); $context->lists->clear(); - ## Define our logic operators - # (for now...) - # all binary operators have the same precedence and process left-to-right - # any parens to the right must be preserved with consecutive binary ops + # Define our logic operators $context->operators->are( 'or' => { class => 'context::Boolean::BOP::or', @@ -34,7 +155,7 @@ sub Init { string => ' or ', TeX => '\vee ', perl => '||', - alternatives => ["\x{2228}"], + # alternatives => ["\x{2228}"], }, 'and' => { class => 'context::Boolean::BOP::and', @@ -45,7 +166,7 @@ sub Init { string => ' and ', TeX => '\wedge ', perl => '&&', - alternatives => ["\x{2227}"], + # alternatives => ["\x{2227}"], }, 'xor' => { class => 'context::Boolean::BOP::xor', @@ -56,7 +177,7 @@ sub Init { string => ' xor ', perl => '!=', TeX => '\oplus ', - alternatives => [ "\x{22BB}", "\x{2295}" ], + # alternatives => [ "\x{22BB}", "\x{2295}" ], }, 'not' => { class => 'context::Boolean::UOP::not', @@ -66,44 +187,57 @@ sub Init { string => 'not ', TeX => '\mathord{\sim}', perl => '!', - alternatives => ["\x{00AC}"], + # alternatives => ["\x{00AC}"], + }, + '`' => { + class => 'context::Boolean::UOP::not', + precedence => 3, + associativity => 'right', + type => 'unary', + string => '`', + TeX => '^\prime', + perl => '!', }, ' ' => { - class => 3, - precedence => 1, + class => 1, + precedence => 3, associativity => 'left', type => 'bin', string => 'and', hidden => 1 }, - '*' => { alias => 'and' }, - '/\\' => { alias => 'and' }, - '+' => { alias => 'or' }, - '\\/' => { alias => 'or' }, - '-' => { alias => 'not' }, - '!' => { alias => 'not' }, - '~' => { alias => 'not', alternatives => ["\x{223C}"] }, - '><' => { alias => 'xor' }, + '*' => { alias => 'and' }, + '/\\' => { alias => 'and' }, + 'wedge' => { alias => 'and', alternatives => ["\x{2227}"] }, + '+' => { alias => 'or' }, + '\\/' => { alias => 'or' }, + 'vee' => { alias => 'or', alternatives => ["\x{2228}"] }, + '-' => { alias => 'not', alternatives => ["\x{00AC}"] }, + '!' => { alias => 'not' }, + '~' => { alias => 'not', alternatives => ["\x{223C}"] }, + '\'' => { alias => '`' }, + '><' => { alias => 'xor' }, + 'oplus' => { alias => 'xor', alternatives => [ "\x{22BB}", "\x{2295}" ] }, ); - ## redefine, but disable some usual context tokens for 'clearer' error messages + # redefine, but disable, some usual context tokens for 'clearer' error messages $context->operators->redefine([ ',', 'fn' ], from => 'Numeric'); $context->lists->redefine('List', from => 'Numeric'); $context->operators->redefine([ '/', '^', '**' ], from => 'Numeric'); $context->operators->undefine('/', '^', '**'); delete $context->operators->get('/')->{space}; - ## Set default variables 'p' and 'q' + # Set default variables 'p' and 'q' $Parser::Context::Variables::type{Boolean} = $Parser::Context::Variables::type{Real}; $context->variables->are( p => 'Boolean', q => 'Boolean', ); - ## Set up new reduction rules: + # Set up new reduction rules: $context->reductions->set('x||1' => 1, 'x||0' => 1, 'x&&1' => 1, 'x&&0' => 1, '!!x' => 1); - ## Define constants for 'True' and 'False' + # Define constants for 'True' and 'False' $context->constants->{namePattern} = qr/(?:\w|[\x{22A4}\x{22A5}])+/; $context->constants->are( T => { @@ -126,14 +260,14 @@ sub Init { 'False' => { alias => 'F' }, ); - ## add our methods to this context + # add our methods to this context bless $context, 'context::Boolean::Context'; - ## allow authors to create Boolean values + # allow authors to create Boolean values main::PG_restricted_eval('sub Boolean { Value->Package("Boolean()")->new(@_) }'); } -## top-level access to context-specific T and T +# top-level access to context-specific T and F sub T { my $context = main::Context(); Value::Error("Context must be a Boolean context") unless $context->can('T'); @@ -146,7 +280,7 @@ sub F { return $context->F; } -## Subclass the Parser::Context to override copy() and add T and F functions +# Subclass the Parser::Context to override copy() and add T and F functions package context::Boolean::Context; our @ISA = ('Parser::Context'); @@ -160,11 +294,11 @@ sub copy { return $self; } -## Access to the constant T and F values +# Access to the constant T and F values sub F { shift->constants->get('F')->{value} } sub T { shift->constants->get('T')->{value} } -## Easy setting of precedence to different types +# Easy setting of precedence to different types sub setPrecedence { my ($self, $order) = @_; if ($order eq 'equal') { @@ -186,7 +320,7 @@ sub setPrecedence { } } -## Subclass Parser::Number to return the constant T or F +# Subclass Parser::Number to return the constant T or F package context::Boolean::Number; our @ISA = ('Parser::Number'); @@ -200,11 +334,11 @@ sub perl { return $self->context->constants->get(('F', 'T')[ $self->{value} ])->{perl}; } -## Subclass Value::Formula for boolean formulas +# Subclass Value::Formula for boolean formulas package context::Boolean::Formula; our @ISA = ('Value::Formula'); -## use every combination of T/F across all variables +# use every combination of T/F across all variables sub createRandomPoints { my $self = shift; my $context = $self->{context}; @@ -230,6 +364,29 @@ sub createRandomPoints { return \@points; } +sub createPointValues { + my $self = shift; + my $context = $self->context; + my $points = shift || $self->{test_points} || $self->createRandomPoints; + my @vars = $context->variables->variables; + my @params = $context->variables->parameters; + + my $f = $self->{f}; + $f = $self->{f} = $self->perlFunction(undef, [ @vars, @params ]) unless $f; + + my (@values, $v); + foreach my $p (@$points) { + $v = eval { &$f(@$p) }; + Value::Error("Can't evaluate formula on test point (%s)", join(',', @{$p})) unless (defined $v); + push @values, $v; + } + + $self->{test_points} = $points; + $self->{test_values} = \@values; + + return \@values; +} + package context::Boolean::BOP; our @ISA = qw(Parser::BOP); @@ -250,6 +407,20 @@ sub perl { return "($result ? context::Boolean->T : context::Boolean->F)"; } +# remove once UOP::string passses 'same' as second argument +sub string { + my ($self, $precedence, $showparens, $position, $outerRight) = @_; + $showparens = "same" if !($position // '') && !($showparens // ''); + return $self->SUPER::string($precedence, $showparens, $position, $outerRight); +} + +# remove once UOP::TeX passses 'same' as second argument +sub TeX { + my ($self, $precedence, $showparens, $position, $outerRight) = @_; + $showparens = "same" if !($position // '') && !($showparens // ''); + return $self->SUPER::TeX($precedence, $showparens, $position, $outerRight); +} + package context::Boolean::BOP::or; our @ISA = qw(context::Boolean::BOP); @@ -314,9 +485,10 @@ sub _check { } sub _reduce { - my $self = shift; - my $reduce = $self->context->{reduction}; - my $op = $self->{op}; + my $self = shift; + my $context = $self->context; + my $reduce = $context->{reduction}; + my $op = $self->{op}; if ($op->isNeg && $reduce->{'!!x'}) { delete $op->{op}{noParens}; @@ -324,7 +496,7 @@ sub _reduce { } if ($op->{isConstant} && $context->flag('reduceConstants')) { - return $self->Item('Value')->new($self->{equation}, [ 1 - $op->value ]); + return $self->Item('Value')->new($self->{equation}, [ 1 - $op->eval ]); } return $self; } @@ -372,7 +544,7 @@ sub compare { return $l->value <=> $r->value; } -## use the context settings +# use the context settings sub string { my $self = shift; my $const = $self->context->constants; @@ -381,7 +553,7 @@ sub string { return ($F, $T)[ $self->value ]; } -## use the context settings +# use the context settings sub TeX { my $self = shift; my $const = $self->context->constants; From 330f1a72fad86cee4c7a26f89d8423d6436fee1b Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Tue, 2 Jul 2024 14:35:57 -0400 Subject: [PATCH 4/5] fix oxan precedence, fix false strings & TeX, and mqOpts spaceBehavesLikeTab: false --- macros/contexts/contextBoolean.pl | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/macros/contexts/contextBoolean.pl b/macros/contexts/contextBoolean.pl index 5e22f80b6f..c6d80fccf2 100644 --- a/macros/contexts/contextBoolean.pl +++ b/macros/contexts/contextBoolean.pl @@ -306,14 +306,18 @@ sub setPrecedence { or => { precedence => 3 }, xor => { precedence => 3 }, and => { precedence => 3 }, + ' ' => { precedence => 3 }, not => { precedence => 3 }, + '`' => { precedence => 3 }, ); } elsif ($order eq 'oxan') { $self->operators->set( or => { precedence => 1 }, xor => { precedence => 2 }, and => { precedence => 3 }, + ' ' => { precedence => 3 }, not => { precedence => 6 }, + '`' => { precedence => 6 }, ); } else { Value::Error("Unknown precedence class '%s'", $order); @@ -338,6 +342,8 @@ sub perl { package context::Boolean::Formula; our @ISA = ('Value::Formula'); +sub cmp_defaults { return (shift->SUPER::cmp_defaults(@_), mathQuillOpts => '{spaceBehavesLikeTab: false}') } + # use every combination of T/F across all variables sub createRandomPoints { my $self = shift; @@ -548,8 +554,8 @@ sub compare { sub string { my $self = shift; my $const = $self->context->constants; - my $T = $const->get('T')->{string} || 'T'; - my $F = $const->get('F')->{string} || 'F'; + my $T = $const->get('T')->{string} // 'T'; + my $F = $const->get('F')->{string} // 'F'; return ($F, $T)[ $self->value ]; } @@ -557,8 +563,8 @@ sub string { sub TeX { my $self = shift; my $const = $self->context->constants; - my $T = $const->get('T')->{TeX} || '\top'; - my $F = $const->get('F')->{TeX} || '\bot'; + my $T = $const->get('T')->{TeX} // '\top'; + my $F = $const->get('F')->{TeX} // '\bot'; return ($F, $T)[ $self->value ]; } From 7735f9f48a72795e6d372ff1b4d3449a3472ed95 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Tue, 2 Jul 2024 14:46:42 -0400 Subject: [PATCH 5/5] embrace the true false --- macros/contexts/contextBoolean.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macros/contexts/contextBoolean.pl b/macros/contexts/contextBoolean.pl index c6d80fccf2..a3bdcf7e65 100644 --- a/macros/contexts/contextBoolean.pl +++ b/macros/contexts/contextBoolean.pl @@ -342,7 +342,7 @@ sub perl { package context::Boolean::Formula; our @ISA = ('Value::Formula'); -sub cmp_defaults { return (shift->SUPER::cmp_defaults(@_), mathQuillOpts => '{spaceBehavesLikeTab: false}') } +sub cmp_defaults { return (shift->SUPER::cmp_defaults(@_), mathQuillOpts => { spaceBehavesLikeTab => \0 }) } # use every combination of T/F across all variables sub createRandomPoints {