The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

#!/usr/bin/perl -w
# Copyright 2014, 2015 Kevin Ryde
# This file 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.
#
# This file 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 this file. If not, see <http://www.gnu.org/licenses/>.
use 5.006;
use strict;
use Carp 'croak';
use FindBin;
use List::Util 'max';
# uncomment this to run the ### lines
# use Smart::Comments;
our $VERSION = 0;
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
# in $str change any 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 dot unchanged
}ge;
return $str;
}
my $comment_prefix_re
= qr{^\s*
([\#%*]+ # # Perl, % TeX, * C continuing
|//+ # // C++
|/\*+ # /* C
|=for\s # =for Perl POD
)? # or nothing
}x;
my $tex_measure_re = qr/[0-9.]*(em|ex|pt|mm|cm|in)/;
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: $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;
};
my $value_maybe;
my $whitespace = sub {
# ignored stuff
$str =~ /\G(
\s+ # whitespace
|\\[,;] # \, \; TeX
|\\kern$tex_measure_re # \kern1.5em
|\\hspace\{$tex_measure_re} # \hspace{1.5em}
|\\(degree|dots[bc]?|q?quad) # \degree \dotsc \dotsb \quad \qquad
|\\phantom\{[^}]*\} # \phantom{...}
)*/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) = @_;
my $output_fh;
my $runner_tempfh;
if ($action eq 'run') {
$output_fh = File::Temp->new (TEMPLATE => 'gp-inline-XXXXXX',
SUFFIX => '.gp',
TMPDIR => 1);
} else {
$output_fh = \*STDOUT;
}
my $test_last_fh = File::Temp->new (TEMPLATE => 'gp-inline-XXXXXX',
SUFFIX => '.gp',
TMPDIR => 1);
my $test_last;
my $output = sub {
my $fh = ($test_last ? $test_last_fh : $output_fh);
print $fh @_
or die "Error writing: $!";
};
my $output_test = sub {
if ($action ne 'defines') {
$output->(@_);
}
};
$output->(<<'HERE');
/* gp-inline test boilerplate begin */
gp_inline__location = "";
gp_inline__bad_location = "";
gp_inline__notbool_location = "";
gp_inline__good = 0;
gp_inline__bad = 0;
gp_inline__check(location,bool) =
{
gp_inline__location = location;
check(bool);
}
check(bool) =
{
/* use "===" so that a vector like [1] is not reckoned as success */
if(bool===1, gp_inline__good++,
bool===0, gp_inline__bad++;
if(gp_inline_location!=gp_inline__bad_location,
print(gp_inline__location": gp-inline fail"),
gp_inline__bad_location=gp_inline_location),
gp_inline__bad++;
if(gp_inline_location!=gp_inline__notbool_location,
print(gp_inline__location": gp-inline expected result 0 or 1, got ",
bool);
gp_inline__notbool_location = gp_inline_location)
);
}
/* 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{(?<prefix>$comment_prefix_re)\s*GP-(?<type>[-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
my $location = gp_quote("$filename:$within_linenum");
$output_test->("$line;\n",
"gp_inline__check($location, 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=",
gp_quote("$filename:$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;
# $line =~ s/(^|[^\\])(\\\\)*%.*//; # % comments
# $line =~ s/\\[,;]/ /g; # ignore \, or \; spacing
# $line =~ s/\\(phantom|hspace){[^}]*}/ /g; # ignore TeX \phantom{...}
# $line =~ s/\\(kern)-?[0-9.]+[a-z]+/ /g; # ignore TeX \kern...
# $line =~ s/\{([+-])\}/$1/g; # {+} or {-}
# $line =~ s/&/,/g; # & as field separator
# $line =~ s|\\[td]?frac(\d)(\d)|($1)/($2)|g; # \frac23
# $line =~ s|\\[td]?frac\{([^}]*)}\{([^}]*)}|($1)/($2)|g; # \frac{}{}
# $line =~ s/\\(sqrt\d+)\s*(i?)/$1$2/g; # \sqrt2 or \sqrt3 i
# $line =~ s/([0-9.)]+)[ \t]*i/$1*I/g; # complex number 123 i
# $line =~ s/\bi[ \t]*([0-9.]+)/I*$1/g; # complex number i 123
# $line =~ s/([+-])[ \t]*(I)\b/$1$2/g; # complex number +- i 123
# $line =~ s/\bi\b/I/g; # complex number i -> I
# if ($within eq 'MATRIX') {
# $line =~ s/\\\\/;/g; # row separator \\
# } else {
# $line =~ s/;/,/g; # semi as separator
# }
# $line =~ s|[^-+*/^()0-9.I,; \t]||sg; # strip anything else
# $line =~ s/(^|;)(\s*,)+/$1/sg; # strip leading commas
# $line =~ s/,(\s*,)+/,/sg; # strip duplicated commas
# $line =~ s/,[ \t]*$//; # strip trailing commas
# # print "\\ ",$line,"\n";
# $line =~ s/[ \t]*$//; # strip trailing whitespace
# $line = decimals_to_fractions($line);
# if ($line ne '') {
# $output->($join,$line,"\n");
# $join = ($line =~ /;$/ ? "\n" : ",\n");
# }
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: $!";
$output_test->(<<'HERE');
print("Total ",(gp_inline__good+gp_inline__bad)," tests, "gp_inline__good" good, "gp_inline__bad" bad");
if(gp_inline__bad,quit(1))
HERE
if ($action eq 'run') {
$runner_tempfh = File::Temp->new (TEMPLATE => 'gp-inline-XXXXXX',
SUFFIX => '.gp',
TMPDIR => 1);
my $read_filename = gp_quote($output_fh->filename);
print $runner_tempfh <<"HERE";
{
read($read_filename);
}
HERE
# iferr(read($read_filename),err,
# print("rethrow");
# error(err), /* rethrow */
# 0);
# /* print(gp_inline__location,"error reading"); 0 */
$output_fh->flush;
my @command = ('gp',
'--quiet',
'-f', # "fast" do not read .gprc
(defined $stacksize ? ('-s', $stacksize) : ()),
'--default', 'recover=0',
# $runner_tempfh->filename,
$output_fh->filename);
if ($verbose) {
print join(' ',@command),"\n";
}
if (! IPC::Run::run(\@command, '<', File::Spec->devnull)) {
$exit = 1;
}
}
}
# Return $str as a string "$str" for use in a gp script.
# Any " quotes etc in $str are suitably escaped.
sub gp_quote {
my ($str) = @_;
$str =~ s/\"/\\"/g;
return '"'.$str.'"';
}
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 "gp-inline [--options] filename...\n";
my @opts =
(['-h, --help', 'Print this help'],
['-v, --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' },
'gp=s' => \$gp,
stdin => \$stdin,
verbose => \$verbose,
's=i' => \$stacksize,
)
or exit 1;
($stdin || @ARGV) or $help->();
}
if ($stdin) {
test_fh(\*STDIN, '(stdin)');
}
test_files(@ARGV);
exit $exit;
#------------------------------------------------------------------------------
__END__
# } elsif ($arg eq '-dist') {
# $exit = 1;
# require ExtUtils::Manifest;
# my $href = ExtUtils::Manifest::maniread();
# my @filenames = grep m{^lib/.*\.pm$|^[^/]\.pm$}, keys %$href;
# $good &= $class->test_files(@filenames);
# # if ($exit) {
# # $class->diag ("gp-inline total $total_expressions checks in $total_files files");
# # exit($good ? 0 : 1);
# # }
#
# sub diag {
# my $self = shift;
# if (eval { Test::More->can('diag') }) {
# Test::More::diag (@_);
# } else {
# my $msg = join('', map {defined($_)?$_:'[undef]'} @_)."\n";
# # $msg =~ s/^/# /mg;
# print STDERR $msg;
# }
# }
=for stopwords gp Ryde globals backslashing backtrace multi-file multi-line
=head1 NAME
gp-inline -- run Pari/GP code inline in a document
=head1 SYNOPSIS
gp-inline [--options] filename...
=head1 DESCRIPTION
C<gp-inline> extracts and executes Pari/GP code from comments written inline
in a document such as TeX or POD, or even in some C code or similar. This
can be used to check calculations or formulas alongside their statement in a
document. For example in TeX
Blah blah and from which it is seen that $1+1 = 2$.
% GP-Test 1+1 == 2
which is checked by running
gp-inline foo.tex
GP is a mathematical system and these checks will usually be for
mathematical calculations and formulas, but can be useful even for just
basic arithmetic.
=head2 Test
A C<GP-Test> line must evaluate to 0 or 1. The evaluation is inside a
function body so semicolons can separate multiple expressions and the last
is the result.
% GP-Test my(n=5); 2*n^2 + n == 55
Requiring a result 0 or 1 helps avoid mistakes like forgetting "== 123" etc.
The suggestion is not to end C<GP-Test> with a semicolon so that it can be
pasted into GP to see the result when experimenting, but C<gp-inline> works
with or without.
The suggestion is also to keep variables local with C<my> to avoid one test
depending on another accidentally, but that's not enforced. See
L</Definitions> 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<gp-inline> constructs don't work properly
with that yet.
% GP-Test 105 == 3*5*7 /* its prime factors */
Tests are run with C<gp -f> so any F<~/.gprc> or C<$GPRC> file is not
evaluated. This is designed to give consistent test results without
personal preferences wanted for C<gp> interactively etc.
=head2 Prefix
The following prefixes are recognised for a C<GP-Test> 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
=for GP-Test 1+1==2
These are comments in Perl, TeX, C, C++, and Perl POD directive C<=for>. In
C style C</*> an optional trailing C<*/> is stripped. Or its comment parts
can be on separate lines if desired
/*
GP-Test 1+1==2
*/
/*
* GP-Test 1+1==2
*/
A Perl POD C<=for> should be a single line and will usually want a blank
line before and after to be valid POD. Those blanks can be a tedious for
many 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 here is not needed if already after an C<__END__> so not
evaluated by Perl, but it's a good way for human readers to distinguish
those lines from the POD text.
=head2 Definitions
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 in the usual way, 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 ]};
Definition lines can also make initial settings. For example
C<strictargs=1> is a good way to guard against mistakes in function
arguments (assuming you're not deliberately lazy with such things)
% GP-DEFINE default(strictargs,1);
External GP code modules can be included with the usual C<read()>. Normally
this will be in a C<GP-DEFINE>.
% GP-DEFINE read("my-library.gp");
=head2 Test Last
C<GP-Test-Last> tests are run last, after the rest of the input file. This
lets a test precede a formula or data definition it depends on. This
doesn't happen often, usually only when some a document gives examples of a
formula before the full statement. If you keep the function definition with
the statement of the formula then C<GP-Test-Last> allows tests to be written
before.
We will want f(6)=10 and ...
% GP-Test-Last f(6) == 10
The unique function satisfying is then f(n) = 2n - 2.
% GP-DEFINE f(n) = 2*n - 2;
Care should be taken not to redefine globals which C<GP-Test-Last> tests
will use. But it's wise anyway not to change the meaning of globals through
a document so that rearranging sections etc doesn't upset the checks.
=head2 Errors
Syntax errors and type errors in tests and definitions are fatal. The
current implementation runs C<gp --default recover=0> so such problems cause
a non-zero exit code. 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<foo.tex> line 153 was the offending C<GP-Test>.
Errors in C<GP-DEFINE> 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<gp-inline --verbose> to see a C<\e> trace of each
expression. It includes some C<"foo.tex:150"> etc strings which are the
source locations.
(This locations printing is not very good. An equivalent of C<#line> would
help. Or is there a way to insert a print before an error backtrace? An
C<iferr()> trap loses the backtrace.)
=head2 Constants, Vectors and Matrices
Numbers in the document text can be extracted as GP definitions. For
example a constant C<foo=123>,
% GP-CONSTANT foo
123
% GP-END
Or a vector C<bar=[1,2,3]>,
% GP-VECTOR bar
1, 2, 3
% GP-END
Or a matrix C<quux=[1,2; 3,4]>,
% 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 signs, optionally with TeX {}
1.42 decimal fraction
\frac58 TeX \frac, \tfrac, \dfrac
\tfrac{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. The matrix separator C<\\> is
treated as comma in a C<VECTOR>. There should be just one value in a
C<CONSTANT> but leading or trailing commas are ignored.
Decimal fractions C<12.45> become rationals like C<1245/100> to preserve an
exact value. If it's some irrational which has been truncated then staying
it exact lets you make an exact check of all the decimals given.
The number syntax accepted is quite strict. This is designed to ensure
C<gp-inline> doesn't quietly ignore something which it wasn't supposed to.
Various bits of TeX are ignored. These are things often wanted in a list of
numbers. However in general it's best to confine C<GP-CONSTANT> etc to just
the numbers and keep TeX outside.
= initial = sign
&= initial TeX align and = sign
\, \; \quad \qquad various TeX spacing and macros
\kern1.5em measures em,ex,pt,mm,cm,in
\hspace{5pt}
\phantom{...}
\degree
\dotsc \dotsb
C<\kern> should be a single numbered measure C<em>, C<ex>. Don't use a
comma for the decimal, and don't use C<plus> etc calculations.
C<\phantom{}> cannot contain nested C<{ }> braces (though it can contain
equivalent C<\begingroup> and C<\endgroup> if desired).
Comments, both TeX or other styles, cannot be in a list of numbers. Perhaps
this will change.
=head1 Other Notes
When including numbers in a document there's a bit of a choice between
writing them in the document and applying checks, versus generating the
numbers externally and C<#include> (or equivalent) to bring them in. The
latter has the disadvantage of several little files (probably), the former
is a little tedious to write manually but then isn't vulnerable to breakage
in a generator program.
Various arithmetic programs or computer algebra systems could be used in a
similar way to C<gp-inline>. GP has the attraction of a compact syntax for
calculations and new functions, and having a range of arbitrary precision
basic types such as fractions, complex numbers, polynomials, even quads for
exact square roots, plus a lot of number theory things for higher
mathematics.
=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<gp> code from each file and print to standard output.
This output is what C<--run> would run with C<gp -f>.
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 definitions from the given files and print to standard
output.
This is good for extracting definitions so they can be used 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<read()>. Usually it avoids mistakes to keep a definition with the formula
etc in the document. But generic or very large code could be kept separate.
=item C<--help>
Print a brief help message.
=item C<--version>
Print the program version number and exit.
=back
=head1 EXIT CODE
C<gp-inline> exits with 0 if all tests in all files are successful, or
non-zero if any problems.
=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 to C<cat> them
together and pass a single file, possibly to C<gp-inline --stdin>; or use
C<--defines> to get the definitions from one file and do a C<read()> of them
in the next. The latter is good to get the defines out of a main document
which can then be read into a separate work-in-progress document which has
text and tests destined for the main, when ready.
The C<GP-VECTOR> and C<GP-MATRIX> number syntax is not enough for all
purposes. There will always be some layout which is too specific for
C<gp-inline> to recognise. The suggestion in that case is to write
C<GP-DEFINE> beside the relevant values. The duplication is not nice, but
done once and with the define right beside the numbers it's not too bad.
Some sort of C<GP-POLYNOMIAL> to pick a polynomial out some TeX could be
good. It could include polynomial fractions (C<t_RFRAC>) since they're a
native type in GP and suit generating functions.
=head1 SEE ALSO
L<gp(1)>
=head1 HOME PAGE
=head1 LICENSE
Copyright 2015 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 <http://www.gnu.org/licenses/>.
=cut
# Maybe:
# GP-Define foo(x) = x+1;
# GP-Test foo(2) == 3
# GP-Inline for(i=1,10, foo(i)==i+1
# GP-Vector
# GP-End
# GP-Constant
# GP-End
# GP-Matrix
# GP-End
#
# GP-Inline check(bool)
# ... names that won't clash
#
# GP-Define all defines at start then all GP-Inline and GP-Test ?