#!/usr/bin/perl -w
# Copyright 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Kevin Ryde
# This file is part of gp-inline.
# gp-inline is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3, or (at your option) any later
# version.
#
# gp-inline is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with gp-inline. If not, see .
use 5.010; # for $+{} named captures and // operator
use strict;
use warnings;
use Carp 'croak';
use FindBin;
use File::Copy;
use File::Spec;
use File::Temp;
use Getopt::Long;
use List::Util 'max';
use IPC::Run;
# uncomment this to run the ### lines
# use Smart::Comments;
our $VERSION = 10;
my $action = 'run';
my $gp = 'gp';
my $verbose = 0;
my $stdin = 0;
my $stacksize;
my $exit = 0;
my $total_files = 0;
my $total_expressions = 0;
### $action
my $ipc;
my $output_fh;
my $test_last_fh;
# delete temp files on signal
sub signal_handler {
my ($sig) = @_;
### caught signal: $sig
undef $SIG{$sig}; # reset to default handling
# remove temp files, if necessary
eval { $output_fh->DESTROY; };
eval { $test_last_fh->DESTROY; };
### pumpable: $ipc && $ipc->pumpable
eval { $ipc->kill_kill(grace => 2); };
undef $ipc;
kill $sig, $$; # re-raise() signal to kill self
exit 1;
}
# signal handling for SIGINT or SIGTERM, if not already IGNORE
foreach my $sig ('INT', 'TERM') {
### $sig
### initial handler: $SIG{$sig}
unless (defined $SIG{$sig} && $SIG{$sig} eq 'IGNORE') {
$SIG{$sig} = \&signal_handler;
### install handler now: $SIG{$sig}
}
}
# in $str change all decimals 0.123 to fractions (123/1000)
sub decimals_to_fraction {
my ($str) = @_;
$str =~ s{(\d*)\.(\d*)}
{length($1) || length($2)
? "($1$2/1".('0' x length($2)).")"
: "$1.$2" # bare "." no digits unchanged
}ge;
return $str;
}
my $comment_prefix_re
= qr{^\s*
([\#%*]+ # # Perl, % TeX, * C continuing
|//+ # // C++
|/\*+ # /* C
|\\\\+ # \\ GP
|=for\s # =for Perl POD
)? # or nothing
}x;
# A TeX measurement like -2.5em
my $tex_measure_re = qr/-?[0-9.]*(em|ex|pt|mm|cm|in|mu)/;
sub pos_linenum {
my ($str, $pos) = @_;
$pos //= pos($_[0]);
$str = substr($str, 0, $pos);
return 1 + scalar($str =~ tr/\n//);
}
sub parse_constants {
my ($str, %options) = @_;
my $type = $options{'type'};
### parse_constants() ...
### $type
my $bad = sub {
my ($message) = @_;
### pos: pos($str)
my $linenum = $options{'linenum'} + pos_linenum($str, pos($str));
print STDERR "$options{'filename'}:$linenum: parse, $message\n";
$str =~ /\G\s*[^\n]{0,20}/s;
my $near = $&;
if ($near eq '') {
print STDERR " (at end)\n";
} else {
print STDERR " near $&\n";
}
$exit = 1;
};
# |(math)?[rl]lap) # \rlap \mathllap etc
my $value_maybe;
my $whitespace = sub {
# ignored stuff
$str =~ /\G(
\s+ # whitespace
|\\[,>:;] # \, \> \: \; TeX
|\\m?kern$tex_measure_re # \kern1.5em and \mkern
|\\hspace\{$tex_measure_re} # \hspace{1.5em}
|\\(degree|dots[bc]?|q?quad) # \degree \dotsc \dotsb \quad \qquad
|\\v?phantom\{[^}]*\} # \phantom{...}
|\\linebreak(\[\d+\])? # \linebreak[0]
|\\penalty-?\d+ # \penalty10 or \penalty-10
)*/gcsx;
};
my $fraction_maybe = sub {
$whitespace->();
### fraction_maybe(): substr($str, pos($str), 20)
if ($str =~ m/\G(\d+(\.\d*)?|\d*\.\d+)/gc) { # number 123.456
my $number = $1;
### $number
### to: substr($str, pos($str), 20)
return decimals_to_fraction($number);
}
if ($str =~ /\G\\[td]?frac(\d)(\d)/gc) { # TeX \frac34
return "$1/$2";
}
if ($str =~ /\G\\[td]?frac\{/gc) { # TeX \frac{123}{456}
my $num = $value_maybe->();
$whitespace->();
unless ($str =~ /\G\}\s*\{/sgc) { # }{
$bad->("unrecognised \\frac{}{}");
return undef;
}
my $den = $value_maybe->();
$whitespace->();
unless ($str =~ /\G\}/gc) { # }
$bad->("unclosed \\frac{}{}");
}
### end fraction: substr($str, pos($str), 20)
return "($num)/($den)";
}
return undef;
};
my $addend_maybe = sub {
### addend_maybe(): substr($str, pos($str), 20)
my $ret = $fraction_maybe->();
my $complex = 0;
$whitespace->();
### try complex: substr($str, pos($str), 20)
if ($str =~ m/\Gi/gc) {
if (defined $ret) {
$ret .= "*I"; # 123 i
} else {
$ret = "I"; # i alone
}
$complex = 1;
}
### $ret
### $complex
return ($ret, $complex);
};
my $sign_maybe = sub {
$whitespace->();
### sign_maybe(): substr($str, pos($str), 20)
if ($str =~ m{\G(([-+])|\{([-+])\})}gc) {
my $sign = $2 || $3;
### $sign
### leave: substr($str, pos($str), 20)
return $sign;
} else {
return undef;
}
};
$value_maybe = sub {
my $sign = $sign_maybe->();
my ($add,$complex1) = $addend_maybe->();
if (! defined $add) {
if (defined $sign) {
$bad->("unrecognised expression after $sign");
}
return undef;
}
my $ret = $sign || '';
$ret .= $add;
$sign = $sign_maybe->() || return $ret;
($add, my $complex2) = $addend_maybe->();
if (! defined $add) {
$bad->("unrecognised expression after $sign");
return $ret;
}
$ret .= $sign;
$ret .= $add;
if ($complex1 == $complex2) {
$bad->("no arithmetic expressions (only complex numbers)");
}
return $ret;
};
$whitespace->();
$str =~ /\G&?=/gc; # optional initial = or &=
# secret undocumented ...
$whitespace->();
$str =~ /\G[[(]/gcx; # optional initial [ or (
my $separator_maybe = sub {
my $comma;
my $semi;
for (;;) {
### separator_maybe(): substr($str, pos($str), 20)
$whitespace->();
if ($str =~ /\G([,&]|\{,\})/gc) { # & , {,} separator
$comma = ',';
} elsif ($str =~ /\G\\\\/gc) {
if ($type eq 'MATRIX') {
$semi = ';'; # \\ for matrix rows
} else {
$comma = ','; # \\ separator in vector or constant
}
} else {
last;
}
}
return $semi || $comma;
};
$separator_maybe->();
my $ret = $value_maybe->();
if (! defined $ret) {
$bad->("unrecognised expression");
return '';
}
for (;;) {
my $sep = $separator_maybe->() || last;
my $more = $value_maybe->();
if (! defined $more) { last; }
if ($type eq 'CONSTANT') {
$bad->("multiple values in CONSTANT");
last;
}
$ret .= $sep;
$ret .= $more;
}
### end of values: substr($str, pos($str), 20)
# secret undocumented ...
$whitespace->();
$str =~ /\G[])]/gcx; # optional initial ] or )
$whitespace->();
if (pos($str) != length($str)) {
$bad->("unrecognised expression");
}
return $ret;
}
sub test_fh {
my ($fh, $filename) = @_;
if ($action eq 'run') {
$output_fh = File::Temp->new (TEMPLATE => 'gp-inline-XXXXXX',
SUFFIX => '.gp',
TMPDIR => 1);
} else {
$output_fh = \*STDOUT;
}
$test_last_fh = File::Temp->new (TEMPLATE => 'gp-inline-XXXXXX',
SUFFIX => '.gp',
TMPDIR => 1);
my $test_last;
my $output = sub {
my $h = ($test_last ? $test_last_fh : $output_fh);
print $h @_
or die "Error writing: $!";
};
my $output_test = sub {
if ($action ne 'defines') {
$output->(@_);
}
};
# This is thinking about check() suppressing multiple reports on the same
# line for checks done in a loop. Haven't worked out how best to treat
# that though.
#
# gp_inline__bad_location = "";
# gp_inline__notbool_location = "";
# if(gp_inline__location!=gp_inline__bad_location,
# gp_inline__bad_location=gp_inline__location),
# ;print(" ===1")
# ;print(" ===0")
# print(" ===other, cf "gp_inline__notbool_location);
# if(gp_inline__location!=gp_inline__notbool_location,
# gp_inline__notbool_location = gp_inline__location
# print("check bool at "gp_inline__location" "bool);
# any " characters suitably backslash escaped, ready to go within a GP string
my $filename_escaped = $filename;
$filename_escaped =~ s/\"/\\"/g;
$output->(<<'HERE');
/* ------------------------------------------------------------------------- */
/* gp-inline test boilerplate begin */
gp_inline__location = "";
gp_inline__good = 0;
gp_inline__bad = 0;
gp_inline__check(location,bool) =
{
gp_inline__location = location;
check(bool);
}
/* this is thinking about a check() for use in loops or "GP-Inline ..."
testing, but it's not settled so don't rely on it */
check(bool) =
{
/* use "===" so that a vector like [1] or float 1.0 not reckoned success */
if(bool===1, gp_inline__good++,
bool===0, gp_inline__bad++;
print(gp_inline__location": gp-inline fail"),
gp_inline__bad++;
print(gp_inline__location": gp-inline expected result 0 or 1, got ",
bool));
}
/* gp-inline test boilerplate end */
/* ------------------------------------------------------------------------- */
HERE
# Possible equality check instead of "=="
# gp_inline__equal(got,want) =
# {
# if(x==y,gp_inline__good++,
# gp_inline__bad++;
# print(gp_inline__location": gp-inline fail");
# print("got "got);
# print("want "want));
# print1();
# }
if ($verbose) {
$output->("\\e 1\n");
}
{
my $end = '';
my $within = '';
my $within_linenum;
my $within_str;
my $join = '';
my $linenum = 1;
my $prev_type = '';
while (defined (my $line = readline $fh)) {
$linenum = $.;
### $line
### $within
if ($line =~ s{(?$comment_prefix_re)\s*GP-(?[-A-Za-z0-9]+)(:|\s)}{}) {
my $type = $+{'type'};
if ($+{'prefix'} =~ m{/\*}) {
$line =~ s{\*+/\s*$}{}; # strip C comment close */
}
$line =~ s/\n$//;
$type = uc($type);
### $type
if ($type eq 'TEST-LAST') {
$test_last = 1;
$type = 'TEST';
} else {
$test_last = 0;
}
if ($type eq 'END') {
if (defined $end) {
$output->(parse_constants($within_str,
filename => $filename,
linenum => $within_linenum,
type => $within));
$output->($end);
undef $end;
} else {
print STDERR "$filename:$linenum: unexpected GP-END\n";
$exit = 1;
}
$within = '';
next;
}
if ($type eq 'TEST') {
if ($within ne 'TEST') {
if ($within ne '') {
print STDERR "$filename:$linenum: still within $within from line $within_linenum\n";
$exit = 1;
}
$within_linenum = $linenum;
$output_test->("gp_inline__test() = ");
}
if ($line =~ /\\$/) {
### test continues after this line ...
### $line
$within = 'TEST';
$output_test->("$line\n");
} else {
### test ends at this line ...
### $line
# no final : on the filename:linenum so it's disguised from Emacs
# compilation-mode
$output_test->("$line;\n",
"gp_inline__check(\"$filename_escaped:$within_linenum\", gp_inline__test())\n");
$within = '';
}
next;
}
if (! $within && $prev_type eq 'not-gp-inline') {
# location string creation obscured against Emacs compilation-mode
# taking it to be many locations to mark etc
$output->("\ngp_inline__location=\"$filename_escaped:$linenum\";\n");
}
if ($within) {
print STDERR "$filename:$linenum: still within $within from line $within_linenum\n";
$exit = 1;
}
if ($type eq 'DEFINE') {
$output->($line,"\n");
} elsif ($type eq 'INLINE') {
$output_test->($line,"\n");
} elsif ($type eq 'CONSTANT') {
if ($line =~ /^\s*$/) {
print STDERR "$filename:$linenum: missing name for CONSTANT\n";
$exit = 1;
}
$output->("$line = {");
$join = "\n";
$end = "};\n";
$within = 'CONSTANT';
$within_linenum = $linenum;
$within_str = '';
} elsif ($type eq 'VECTOR') {
if ($line =~ /^\s*$/) {
print STDERR "$filename:$linenum: missing name for VECTOR\n";
$exit = 1;
}
$output->("$line = {[");
$join = "\n";
$end = "]};\n";
$within = 'VECTOR';
$within_linenum = $linenum;
$within_str = '';
} elsif ($type eq 'MATRIX') {
if ($line =~ /^\s*$/) {
print STDERR "$filename:$linenum: missing name for MATRIX\n";
$exit = 1;
}
$output->("$line = {[");
$join = "\n";
$end = "]};\n";
$within = 'MATRIX';
$within_linenum = $linenum;
$within_str = '';
} else {
print STDERR "$filename:$linenum: ignoring unrecognised \"$type\"\n";
}
$prev_type = $type;
} elsif ($within eq 'CONSTANT'
|| $within eq 'VECTOR'
|| $within eq 'MATRIX') {
$within_str .= $line;
next;
} else {
### non test line ...
$prev_type = 'not-gp-inline';
}
}
### EOF ...
if ($within) {
print STDERR "$filename:$linenum: end of file within \"$within\"\n";
$exit = 1;
}
}
$test_last = 0;
$output_fh->flush;
$test_last_fh->flush;
File::Copy::copy($test_last_fh->filename, $output_fh)
or die "Error copying Test-Last: $!";
undef $test_last_fh; # remove tempfile
$output_test->(<<"HERE");
print("$filename_escaped ",(gp_inline__good+gp_inline__bad)," tests, "gp_inline__good" good, "gp_inline__bad" bad");
if(gp_inline__bad,quit(1))
HERE
# cleanup the global original location marker
$output->("\nkill(gp_inline__location);\n");
if ($action eq 'run') {
$output_fh->flush;
my @command = ($gp,
'--quiet',
'-f', # "factory" do not read .gprc
(defined $stacksize ? ('-s', $stacksize) : ()),
'--default', 'recover=0',
$output_fh->filename);
if ($verbose) {
print join(' ',@command),"\n";
}
$ipc = IPC::Run::start(\@command, '<', File::Spec->devnull);
if (! $ipc->finish) {
$exit = 1;
}
}
}
sub test_file {
my ($filename) = @_;
### test_file(): $filename
$total_files++;
open my $fh, '<', $filename
or die "Cannot open $filename: $!";
test_fh($fh, $filename);
close $fh
or die "Error closing $filename: $!";
}
sub test_files {
# ($filename, ...)
foreach my $filename (@_) {
test_file($filename);
}
}
#------------------------------------------------------------------------------
# mainline
{
my $help = sub {
print "$FindBin::Script [--options] filename...\n";
my @opts =
(['--help, -?', 'Print this help'],
['--version', 'Print program version'],
['--verbose', 'Print extra messages'],
['--run', 'Run the inline tests in each FILENAME'],
['--extract', 'Print the test code from each FILENAME'],
['--defines', 'Print just the definitions from each FILENAME'],
);
my $width = 2 + max (map { length ($_->[0]) } @opts);
foreach (@opts) {
printf "%-*s%s\n", $width, $_->[0], $_->[1];
}
print "\n";
exit 0;
};
GetOptions ('help|?' => $help,
version => sub {
print "$FindBin::Script version $VERSION\n";
exit 0;
},
run => sub { $action = 'run' },
defines => sub { $action = 'defines' },
extract => sub { $action = 'extract' },
stdin => \$stdin,
verbose => \$verbose,
# undocumented
'gp=s' => \$gp, # only command, no args
's=i' => \$stacksize,
)
or exit 1;
($stdin || @ARGV) or $help->();
}
if ($stdin) {
test_fh(\*STDIN, '(stdin)');
}
test_files(@ARGV);
exit $exit;
#------------------------------------------------------------------------------
__END__
# Maybe checks like .. but want names that won't clash ...
# GP-DEFINE foo(x) = x+1;
# GP-Test foo(2) == 3
# GP-Inline for(i=1,10, check(foo(i)==i+1))
# GP-Inline check(bool)
=for stopwords gp Ryde globals backslashing backtrace multi-file multi-line bignum recognise recognised Equalities equalities TeXisms PariEmacs subprocess eval uncomment parens
=head1 NAME
gp-inline -- run Pari/GP code inline in a document
=head1 SYNOPSIS
gp-inline [--options] filename...
=head1 DESCRIPTION
C extracts and executes Pari/GP code from comments written inline
in a document source such as TeX or POD, or in program source code. This
can be used to check calculations or formulas alongside their statement in a
document or use in a program. For example in TeX
From this proposition it follows $1+1 = 2$.
% GP-Test 1+1 == 2
which is checked by running
gp-inline foo.tex
Pari/GP has bignum integers, fractions, complex numbers, vectors and more
which can be used for simple arithmetic and complicated calculations. It
has a lot of number theory and other mathematics for sophisticated
mathematical checks.
=head1 OPTIONS
The command line options are
=over 4
=item C<--run>
Run the inline tests in each given file. This is the default action.
=item C<--stdin>
Read a document from standard input instead of named files.
=item C<--extract>
Extract the inline C code from each file and print to standard output.
This output is what C<--run> would pass to C.
Usually C<--extract> should be used on just one input file, otherwise the
tests of each file are output one after the other and globals left by the
first might upset later tests.
=item C<--defines>
Extract just the C parts of the given files and print to standard
output.
This is good for using them separately in further calculations or
experiments. It's also possible to go the other way, have definitions in a
separate file which the document loads with C. Usually it avoids
mistakes to keep a definition with the formula etc in the document. But
generic or very large code could be separate.
=item C<--help>
Print a brief help message.
=item C<--verbose>
Run C with command echoing to show the progress it's making or where an
error occurs.
=item C<--version>
Print the program version number and exit.
=back
=head1 INPUT
=head2 GP-Test
A C line must evaluate to 0 or 1. Usually it will be an C<==> or
compare etc, but can be anything 0 or 1. The evaluation is inside a
function body so semicolons can separate a sequence of expressions and the
last is the result.
% GP-Test my(n=5); 2*n^2 + n == 55
Requiring result 0 or 1 helps avoid mistakes like forgetting "== 123" etc.
The suggestion is no final semicolon on a C so it can be copied
into GP to see the result when experimenting, but C works with or
without.
The suggestion is also to keep variables local with C to avoid one test
depending on another accidentally, but that's not enforced. See
L below for making global variables.
Multi-line tests can be written with GP style backslashing
% GP-Test some_thing() \
% GP-Test == 123
Comments can be included in a test in GP C* ... */> style. Don't use
C<\\> style as the expressions C constructs don't work properly
with that yet.
% GP-Test 105 == 3*5*7 /* its prime factors */
A comment should not be the sole content of a test, since C takes an
empty expression as 0 so the test fails.
% GP-Test /* don't put a comment alone */
A GP comment can be part of a test with backslashing if desired.
% GP-Test /* some comment */ \
% GP-Test 1 + 1 == 2
This could just as easily be a document comment, but making it part of the
test expression puts it though to C for human
readability or debugging (but doesn't show in C<--verbose> since C
strips comments from its echos).
Tests are run with C so any user F<~/.gprc> or C<$GPRC> file is not
evaluated. This is designed to give consistent test results, avoiding
personal preferences wanted for C interactively etc.
=head2 Security
C has functions and features to load other GP code, write files, and
spawn shell commands for almost arbitrary system actions, so only run
C on trusted documents etc.
=head2 Prefix
The following prefixes are recognised for a C line (etc)
GP-Test 1+1==2
# GP-Test 1+1==2
% GP-Test 1+1==2
/* GP-Test 1+1==2 */
* GP-Test 1+1==2
// GP-Test 1+1==2
\\ GP-Test 1+1==2
=for GP-Test 1+1==2
These are comments in Perl, TeX, C, C++, GP, and Perl POD directive C<=for>.
In C style C*> an optional trailing C<*/> is stripped. Or C*> and C<*/>
could be their own lines if preferred. This suits a block of several tests
/*
GP-Test 1+1==2
*/
/*
* GP-Test 1+1==2
* GP-Test 2+2==4
*/
A Perl POD C<=for> should be a single line and will usually need a blank
line before and after to be valid POD. Those blanks can be tedious when
writing a few tests and in that case the suggestion is to C<=cut> and write
a block of tests
=cut
# GP-Test 2+2==4
# GP-Test 4+4==8
=pod
The C<#> prefix is not needed if already after an C<__END__> and thus not
evaluated by Perl, but it's a good way for human readers to distinguish
those lines from the POD text.
The prefixes include GP's own C<\\> comment, or GP has C syntax too. GP
tests inline in GP source code are good for the same reasons as other
languages, ie. they're close to relevant code or help text and don't slow
down normal execution. Anything more than some little checks are probably
better in external test code though.
=head2 GP-DEFINE
Definition lines can create new GP functions or globals
% GP-DEFINE my_func(n) = 2*n + 3;
% GP-DEFINE my_vector = [ 1, 2, 3, 5 ];
These lines are arbitrary code passed directly to GP. Generally they should
end with a C<;> to suppress result printing from GP (depending how it's
run), but that's not enforced. Multi-line functions or expressions can use
either backslashing or braces
% GP-DEFINE long_func(n) = \
% GP-DEFINE some + long \
% GP-DEFINE - expression;
% GP-DEFINE my_matrix = {[
% GP-DEFINE 1, 2;
% GP-DEFINE 2, 1
% GP-DEFINE ]};
A definition is usually something used a few times but it can be convenient
to make a definition even when used just once, either matching document text
or to shorten a long test expression.
Definition lines can make GP C settings. For example
C is a good way to guard against mistakes in function arguments
(assuming you're not deliberately lazy or using default zeros)
% GP-DEFINE default(strictargs,1);
External GP code modules can be loaded with the usual C.
% GP-DEFINE read("my-library.gp");
If you write C instead of C by mistake then that test
expression is of course not a test. There's no way for C to
identify that, but if you leave off C<;> semicolons from test lines then it
shows as a stray result print.
=head2 GP-Test-Last
C are tests run at the end of the document, after the rest of
the input file. This lets a test precede the definition of some formula or
data. This doesn't happen often, usually only when a document gives an
example of a formula's use before the full statement. If you keep the
C with the full statement then C allows tests using
it to appear earlier.
We will find below that f(6)=10 ...
% GP-Test-Last f(6) == 10
The unique function satisfying is thus f(n) = 2n - 2.
% GP-DEFINE f(n) = 2*n - 2;
Care should be taken not to redefine globals which C will
need. It's wise anyway not to change the meaning of globals so that
document sections can be rearranged etc without upsetting checks, and so
definitions can be extracted and used for other experiments or tests.
=head2 Test Ranges
For testing a function on a set of values, the suggestion is to use
C to evaluate something like
% GP-Test vector(100,k, fibonacci(k)) == \
% GP-Test vector(100,k, fibonacci(k-1) + fibonacci(k-2))
Or for 2 variables C similarly (or a vector of vectors for varying
second range). If a test fails then relevant parts can be copied into GP to
see the values on each side. Changing "==" to a subtraction "-" can show
differences, to see perhaps only a few are wrong, or everything off by a
constant.
The index variable or variables can be manipulated to test over some range
other than 1 to n. The intention is to have sort of inline test form which
could use C or C since they make test ranges clearer, but
don't know yet how that should look or how to print which index fails etc.
Another approach suitable for powers and linear recurrences is to express
sequences of values in polynomial generating functions (type C).
Equalities within a sequence or between sequences become equalities on the
polynomials with suitable shifts, spreads, etc. A generating function
effectively encapsulates an entire sequence so equality verifies an identity
for all n. This suits sums, index changes, and multiplications by
constants. Term-wise multiplication of linear recurrences are still linear
recurrences.
=head2 Errors
Syntax errors and type errors in tests and definitions are fatal. The
current implementation runs C so such problems cause
an immediate non-zero exit. A location string is included in the test
expression so the backtrace has something like
*** at top-level: ...inline("foo.tex:153",(()->bar())())
...
which means input file F line 153 was the offending C.
Errors in C statements don't have this location in the backtrace
(since they're a "top-level" evaluation). If the offending part is not
obvious then try C to see a GP C<\e> trace of each
expression. It includes some C<"foo.tex:150"> etc location strings. The
usual GP C is available too (which writes to file
F by default).
This location printing is not very good. An equivalent of C<#line> in GP
could help tell it an original location. Or could a print go before an
error backtrace? An C trap doing that loses the backtrace.
=head2 GP-CONSTANT, GP-VECTOR, GP-MATRIX
Numbers in the document text can be extracted as GP definitions. For
example a constant C,
% GP-CONSTANT foo
123
% GP-END
Or vector C,
% GP-VECTOR bar
1, 2, 3
% GP-END
Or matrix C,
% GP-MATRIX quux
1 & 2 \\ 3 & 4
% GP-END
These GP definitions can be used in subsequent tests, and the numbers are
also document text or program code, etc. The number forms accepted are
123 integer
-1, {+}1 signs, optionally with TeX {}
1.42 decimal fraction
\frac58 TeX \frac, \tfrac, \dfrac
\dfrac{12}{34}
-3-4i complex number, lower case i
\tfrac{5}{2+i} fractions with complex numbers
, {,} & vector separator commas
\\ matrix row separator
Multiple commas etc are treated as just one. In C, the matrix
row separator C<\\> is treated like a comma. There should be only one value
in C. Leading and trailing commas are ignored.
Decimal fractions C<12.45> become rationals (type C) like
C<1245/100> to preserve the exact value. If it's actually some irrational
which has been truncated then staying exact lets you make an exact check of
the decimals given, independent of the GP float C. Presently
there's nothing to have comma as decimal point, nor to have comma or other
for a thousands separator.
Complex numbers use lower-case i for the imaginary part, either alone or
after a number. They become type C. Presently there's nothing
to have a different letter like say j.
The number syntax accepted is quite strict. This is designed to ensure
C doesn't quietly ignore something it shouldn't.
Some bits of TeX are ignored. These are things often wanted in a list of
numbers. However in general it's best to confine C etc to just
the numbers and keep TeXisms outside.
= initial = sign
&= initial TeX align and = sign
\, \> \: \; \quad \qquad various TeX spacing and macros
\kern1.5em \mkern1.5mu measures em,ex,pt,mm,cm,in,mu
\hspace{5pt}
\linebreak \linebreak[0]
\penalty-10
\phantom{...}
\vphantom{...}
\degree
\dotsc \dotsb
C<\kern> should be a single numbered measure. Don't use a comma for the
decimal. C<\phantom{}> cannot contain further nested C<{ }> braces (though
it could contain equivalent C<\begingroup> and C<\endgroup> if really
needed).
Comments, both TeX and other styles, cannot be in a list of numbers.
This number extraction system is not enough for all purposes. There will
always be some expression, markup, or layout, which is too specific for
C to recognise. The suggestion in that case is to write a
suitable corresponding C beside the values. Duplicating
expressions like that is not nice, but done once and with the define right
beside the numbers it's not too bad.
% GP-DEFINE sqrt5 = quadgen(20);
% GP-Test sqrt5^2 == 5
The golden ratio is $\phi = \frac{1+\sqrt{5}}{2}$.
% GP-DEFINE phi = ( 1+ sqrt5 )/2;
=cut
# GP-Test quadgen(20)^2 == 5
# phi*1.0
=pod
=head2 Other Notes
When including numbers in a document or program there's a choice between
copying them into the document and applying checks, versus generating the
numbers externally and a C<#include> or equivalent to bring them in. An
C has the disadvantage of several little files (probably). Inline
is tedious to insert manually but then isn't vulnerable to subtle breakage
in generator programs.
Calculations in floating point often require some sort of C
test. It might go by difference or by ratio, and how much accuracy to
expect depends on how much error is likely to accumulate and how much GP
precision is selected. Such tests can be useful for trigonometry or
polynomial roots to at least guard against a wildly wrong formula. (For a
single algebraic irrational, GP C etc can do exact calculations.)
Various computer arithmetic or computer algebra systems could be used in a
similar way to C. GP has the attraction of a compact syntax for
expressions and new functions, and a range of arbitrary precision basic
types, including exact real and complex square roots, plus a lot of number
theory for higher mathematics.
If you're new to GP then a couple of subtleties include
=over
=item
C<,> which separates parts of an C or C is "stronger" than the
C<;> between multiple statements within those parts. Once you realize the
argument to those is a list of statements separated by C<;> then there's no
difficulty.
=item
C<++> and C<--> are written like C post-increment but they are pre-increment
(and decrement). Of course you don't have to use them in the middle of an
expression if it might be unclear if you don't want to.
=item
C is written with the condition first, but the
statements execute first and then the condition is evaluated. Condition
first matches C, but take note that the execution order is reversed
as well as the sense of the condition.
=back
=head1 EMACS
An Emacs C is included in the C sources. Its
C runs a test line with GP in a PariEmacs subprocess.
If the result is false then you might correct either the test or the text.
This eval works on ordinary comment lines too so can be used for GP
expressions left as experiments in a document.
C extracts and passes all C,
C etc to PariEmacs (all of C). This can
load definitions needed to run test lines or to experiment.
Generally Emacs' parenthesis matching doesn't work across multiple C<%> or
C<#> etc comment lines. When editing a multi-line C or
C it can help to temporarily uncomment (C) in order to fix or apply parens to a deeply nested
expression. Usually C etc keywords can be left while doing this,
just the comment prefix removed.
A pattern for the error message in L above is added to
C (until perhaps hopefully in the future that message
could become simply C). Alas however a filename longer than 19
characters is truncated by GP in its output so is not matched.
=head1 EXIT CODE
C exits with 0 if all tests in all files are successful, or
non-zero if any problems.
=head1 SIGNAL HANDLING
Signals C and C are caught and the C subprocess is
forcibly killed if it hasn't already died from the signal.
It's possible for the C sub-process to run a sub-sub-process by GP
C etc. C doesn't know this and doesn't wait. This may
mean C returns to a shell prompt with the sub-sub-process still
dying. Hopefully this is uncommon and no worse than some stray output to
the terminal.
The current implementation extracts tests to temporary files (in whatever
system-dependent C directory). Tests are run by having C read one
of these files. All temporaries are removed on normal exit or C or
C. Running C on a file is preferred over say a pipe, since it
should be more portable.
=head1 ENVIRONMENT
=over 4
=item C, C, C, etc
Usual directory for temporary files per L (which in turn per
L C).
=back
=head1 BUGS
There's no support for a multi-file document where defines would be carried
over from one part to the next. The suggestion is either C all
together and pass to C; or use C<--defines> to extract the
definitions from one file and have a C of them in the next. The
latter approach is good for getting main document definitions into separate
work-in-progress too.
Some sort of C to pick a polynomial out of some TeX could be
good. The syntax recognised would have to be quite restricted but could
include polynomial fractions (GP type C).
Some sort of C<--secure> to run C with C might be good for
executing tests in a document not fully trusted.
=head1 SEE ALSO
L
=head1 HOME PAGE
L
=head1 LICENSE
Copyright 2015, 2016, 2017, 2018, 2019, 2020, 2021 Kevin Ryde
gp-inline is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free
Software Foundation; either version 3, or (at your option) any later
version.
gp-inline is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
You should have received a copy of the GNU General Public License along
with gp-inline. If not, see L.
=cut