1
0
Fork 0
mirror of https://github.com/ganelson/inform.git synced 2024-07-05 08:34:22 +03:00
inform7/retrospective/6L02/I6T/RealNumber.i6t
2019-02-05 00:44:07 +00:00

633 lines
16 KiB
Plaintext

B/rnumt: Real Number Template.
@Purpose: Support for real numbers.
@-------------------------------------------------------------------------------
@p Printing reals.
Most of the code in this section is by Andrew Plotkin, and derives from test
cases used to check the floating-point extensions to Glulx.
@c
#Ifdef TARGET_GLULX;
[ REAL_NUMBER_TY_Say fp;
print (Float) fp;
];
[ REAL_NUMBER_TY_Compare r1 r2;
@jflt r1 r2 ?less;
@jfeq r1 r2 0 ?same;
return 1;
.same; return 0;
.less; return -1;
];
[ NUMBER_TY_to_REAL_NUMBER_TY int real; @numtof int real; return real; ];
[ REAL_NUMBER_TY_to_NUMBER_TY real int; @ftonumn real int; return int; ];
[ REAL_NUMBER_TY_Sin in out; @sin in out; return out; ];
[ REAL_NUMBER_TY_Cos in out; @cos in out; return out; ];
[ REAL_NUMBER_TY_Tan in out; @tan in out; return out; ];
[ REAL_NUMBER_TY_Arcsin in out; @asin in out; return out; ];
[ REAL_NUMBER_TY_Arccos in out; @acos in out; return out; ];
[ REAL_NUMBER_TY_Arctan in out; @atan in out; return out; ];
[ REAL_NUMBER_TY_Sinh in tmp out;
@exp in tmp;
@fsub M_0 in in;
@exp in out;
@fadd tmp out out;
@fmul out M_HALF out;
return out;
];
[ REAL_NUMBER_TY_Cosh in tmp out;
@exp in tmp;
@fsub M_0 in in;
@exp in out;
@fsub tmp out out;
@fmul out M_HALF out;
return out;
];
[ REAL_NUMBER_TY_Tanh in tmp out;
tmp = REAL_NUMBER_TY_Sinh(in);
in = REAL_NUMBER_TY_Cosh(in);
@fdiv tmp in out;
return out;
];
[ REAL_NUMBER_TY_Reciprocal in out; @fdiv M_1 in out; return out; ];
[ REAL_NUMBER_TY_Negate in out; @fsub M_0 in out; return out; ];
[ REAL_NUMBER_TY_Plus x y out; @fadd x y out; return out; ];
[ REAL_NUMBER_TY_Minus x y out; @fsub x y out; return out; ];
[ REAL_NUMBER_TY_Times x y out; @fmul x y out; return out; ];
[ REAL_NUMBER_TY_Divide x y out; @fdiv x y out; return out; ];
[ REAL_NUMBER_TY_Remainder x y r q; @fmod x y r q; return r; ];
[ REAL_NUMBER_TY_Approximate x y quotient out;
@fdiv x y quotient;
@fadd quotient M_HALF quotient;
@floor quotient quotient;
@fmul quotient y out;
return out;
];
[ REAL_NUMBER_TY_Root x out; @sqrt x out; return out; ];
[ REAL_NUMBER_TY_Cube_Root x out; @pow x M_THIRD out; return out; ];
[ REAL_NUMBER_TY_Pow x y out; @pow x y out; return out; ];
[ REAL_NUMBER_TY_Exp x out; @exp x out; return out; ];
[ REAL_NUMBER_TY_Log x out; @log x out; return out; ];
[ REAL_NUMBER_TY_BLog x n d out;
@log x out;
if (n == 10) d = M_LOG10;
else {
@numtof n d;
@log d d;
}
@fdiv out d out;
return out;
];
[ REAL_NUMBER_TY_Floor x out; @floor x out; return out; ];
[ REAL_NUMBER_TY_Ceiling x out; @ceil x out; return out; ];
[ REAL_NUMBER_TY_Abs x; return x & $7fffffff; ];
[ REAL_NUMBER_TY_Nan x; @jisnan x ?Nan; rfalse; .Nan; rtrue; ];
Constant M_0 = $0;
Constant M_1 = $3F800000;
Constant M_HALF = $3F000000; ! 1/3
Constant M_THIRD = $3EAAAAAB; ! 1/3
Constant M_LOG10 = $40135D8E; ! log(10)
Constant M_N1 = $BF800000; ! -1
Constant M_PI = $40490FDB;
Constant M_NPI = $C0490FDB;
Constant M_2PI = $40C90FDB; ! 2*pi
Constant M_PI2 = $3FC90FDB; ! pi/2
Constant M_NPI2 = $BFC90FDB;
Constant M_E = $402DF854;
Constant M_E2 = $40EC7326; ! e^2
Constant M_N0 = $80000000; ! negative zero
Constant M_INF = $7F800000; ! infinity
Constant M_NINF = $FF800000; ! negative infinity
Constant M_NAN = $7F800001; ! one of many NaN values
Constant M_NNAN = $FF800001; ! another, with a sign bit
! Floating-point parsing routines.
! Parse a float from a text buffer. Returns a float value, or FLOAT_NAN if
! no value was understood.
!
! The recognized format, if you'll pardon a slightly bastardized regexp
! syntax, is "S?D*(PD*)?(ES?D+)?" where S is a sign character "+" or "-",
! D is a decimal digit "0" to "9", P is a decimal point ".",
! and E is the exponential modifier "E" or "e".
!
! For flexibility, the string "M10^" is also accepted for E, where M is
! "X", "x", "*", or the multiplication sign @{D7}. Optional spaces are
! allowed before and after the M sign. (But only for the "10^" form of
! the exponent, not the "e" form.)
!
! This routine does not try to recognize special names for infinity or NaN,
! but it can return FLOAT_INFINITY or FLOAT_NINFINITY if the exponent is too
! large.
!
! This routine relies on floating-point math. Therefore, the same string
! may parse to slightly different float values on different interpreters!
! Be warned.
!
! If useall is true, this insists on using all len characters from the buffer.
! (It returns FLOAT_NAN if any unrecognized characters are left over.)
! Contrariwise, if useall is false, unused characters at the end of the buffer
! are fine. (But not at the beginning; the float must start at the beginning
! of the buffer.)
!
[ FloatParse buf len useall
res ix val ch ten negative intpart fracpart fracdiv
expon expnegative count;
! print "FloatParse <";
! for (ix=0: ix<len: ix++) print (char) buf->ix;
! print ">^";
if (len == 0)
return FLOAT_NAN;
ix = 0;
negative = false;
intpart = 0;
fracpart = 0;
@numtof 10 ten;
! Sign character (optional)
ch = buf->ix;
if (ch == '-') {
negative = true;
ix++;
}
else if (ch == '+') {
ix++;
}
! Some digits (optional)
for (count=0 : ix<len : ix++, count++) {
ch = buf->ix;
if (ch < '0' || ch > '9')
break;
val = (ch - '0');
@numtof val val;
@fmul intpart ten intpart;
@fadd intpart val intpart;
}
! Decimal point and more digits (optional)
if (ix<len && buf->ix == '.') {
ix++;
@numtof 1 fracdiv;
for ( : ix<len : ix++, count++) {
ch = buf->ix;
if (ch < '0' || ch > '9')
break;
val = (ch - '0');
@numtof val val;
@fmul fracpart ten fracpart;
@fadd fracpart val fracpart;
@fmul fracdiv ten fracdiv;
}
@fdiv fracpart fracdiv fracpart;
}
! If there are no digits before *or* after the decimal point, fail.
if (count == 0)
return FLOAT_NAN;
! Combine the integer and fractional parts.
@fadd intpart fracpart res;
! Exponent (optional)
if (ix<len && buf->ix == 'e' or 'E' or ' ' or '*' or 'x' or 'X' or $D7) {
if (buf->ix == 'e' or 'E') {
! no spaces, just the 'e'
ix++;
if (ix == len)
return FLOAT_NAN;
}
else {
! any number of spaces, "*", any number of spaces more, "10^"
while (ix < len && buf->ix == ' ')
ix++;
if (ix == len)
return FLOAT_NAN;
if (buf->ix ~= '*' or 'x' or 'X' or $D7)
return FLOAT_NAN;
ix++;
while (ix < len && buf->ix == ' ')
ix++;
if (ix == len)
return FLOAT_NAN;
if (buf->ix ~= '1')
return FLOAT_NAN;
ix++;
if (buf->ix ~= '0')
return FLOAT_NAN;
ix++;
if (buf->ix ~= $5E)
return FLOAT_NAN;
ix++;
}
! Sign character (optional)
expnegative = false;
ch = buf->ix;
if (ch == '-') {
expnegative = true;
ix++;
}
else if (ch == '+') {
ix++;
}
expon = 0;
! Some digits (mandatory)
for (count=0 : ix<len : ix++, count++) {
ch = buf->ix;
if (ch < '0' || ch > '9')
break;
expon = 10*expon + (ch - '0');
}
if (count == 0)
return FLOAT_NAN;
if (expnegative)
expon = -expon;
if (expon) {
@numtof expon expon;
@pow ten expon val;
@fmul res val res;
}
}
if (negative) {
! set the value's sign bit
res = $80000000 | res;
}
if (useall && ix ~= len)
return FLOAT_NAN;
return res;
];
! An I6 grammar routine (GPR) for floats. On success, this returns
! GPR_NUMBER and stores a value in the global parsed_number.
!
! This is quite a nuisance, actually, because "." is a word separator.
! Also, we want to accept command sequences like "type 4. look"! So we
! need to collect a set of words made up of digits, signs, periods, and
! the letter "e", but without any intervening whitespace, and excluding
! a trailing period.
!
! (This will fail to correctly parse "type 4.e", but I think that is a
! small flaw. A player would more likely try "type 4. e" or, really,
! not concatenate commands at all. It will also parse "type 4. on keyboard"
! as two commands, even though "4." is a legitimate float literal.
! Contrariwise, "type 4. x me" will be taken as one command. (Because the "x"
! *could* be a continuation of the float, and I don't back up when it turns
! out not to be.) I don't plan to worry about these cases.)
[ FLOAT_TOKEN buf bufend ix ch firstwd newstart newlen lastchar lastwasdot;
if (wn > num_words)
return GPR_FAIL;
! We're going to collect a set of words. Start with zero words.
firstwd = wn;
buf = WordAddress(wn);
bufend = buf;
lastchar = 0;
while (wn <= num_words) {
newstart = WordAddress(wn);
if (newstart ~= bufend) {
! There's whitespace between the previous word and this one.
! Whitespace is okay around an asterisk...
if ((lastchar ~= '*' or 'x' or 'X' or $D7)
&& (newstart->0 ~= '*' or 'x' or 'X' or $D7)) {
! But around any other character, it's not.
! Don't include the new word.
break;
}
}
newlen = WordLength(wn);
for (ix=0 : ix<newlen : ix++) {
ch = newstart->ix;
if (~~((ch >= '0' && ch <= '9')
|| (ch == '-' or '+' or 'E' or 'e' or '.' or 'x' or 'X' or '*' or $D7 or $5E)))
break;
}
if (ix < newlen) {
! This word contains an invalid character.
! Don't include the new word.
break;
}
! Okay, include it.
bufend = newstart + newlen;
wn++;
lastchar = (bufend-1)->0;
lastwasdot = (newlen == 1 && lastchar == '.');
}
if (wn > firstwd && lastwasdot) {
! Exclude a trailing period.
wn--;
bufend--;
}
if (wn == firstwd) {
! No words accepted.
return GPR_FAIL;
}
parsed_number = FloatParse(buf, bufend-buf, true);
if (parsed_number == FLOAT_NAN)
return GPR_FAIL;
return GPR_NUMBER;
];
! Floating-point printing routines. (These are based on code in
! Glulxercise.inf, but modified.)
! Print a float. This uses exponential notation ("[-]N.NNNe[+-]NN") if
! the exponent is not between 6 and -4. If it is (that is, if the
! absolute value is near 1.0) then it uses decimal notation ("[-]NNN.NNNNN").
! The precision is the number of digits after the decimal point
! (at least one, no more than eight). The default is five, because
! beyond that rounding errors creep in, and even exactly-represented
! float values are printed with trailing fudgy digits.
! Trailing zeroes are trimmed.
[ Float val prec pval;
pval = val & $7FFFFFFF;
@jz pval ?UseFloatDec;
@jfge pval $49742400 ?UseFloatExp; ! 1000000.0
@jflt pval $38D1B717 ?UseFloatExp; ! 0.0001
.UseFloatDec;
return FloatDec(val, prec);
.UseFloatExp;
return FloatExp(val, prec);
];
Array PowersOfTen --> 1 10 100 1000 10000 100000 1000000 10000000 100000000 1000000000;
! Print a float in exponential notation: "[-]N.NNNe[+-]NN".
! The precision is the number of digits after the decimal point
! (at least one, no more than eight). The default is five, because
! beyond that rounding errors creep in, and even exactly-represented
! float values are printed with trailing fudgy digits.
! Trailing zeroes are trimmed.
[ FloatExp val prec log10val expo fexpo idig ix pow10;
if (prec == 0)
prec = 5;
if (prec > 8)
prec = 8;
pow10 = PowersOfTen --> prec;
! Knock off the sign bit first.
if (val & $80000000) {
@streamchar '-';
val = val & $7FFFFFFF;
}
@jisnan val ?IsNan;
@jisinf val ?IsInf;
if (val == $0) {
expo = 0;
idig = 0;
jump DoPrint;
}
! Take as an example val=123.5, with precision=6. The desired
! result is "1.23000e+02".
@log val sp;
@fdiv sp $40135D8E log10val; ! $40135D8E is log(10)
@floor log10val fexpo;
@ftonumn fexpo expo;
! expo is now the exponent (as an integer). For our example, expo=2.
@fsub log10val fexpo sp;
@numtof prec sp;
@fadd sp sp sp;
@fmul sp $40135D8E sp;
@exp sp sp;
! The stack value is now exp((log10val - fexpo + prec) * log(10)).
! We've shifted the decimal point left by expo digits (so that
! it's after the first nonzero digit), and then right by prec
! digits. In our example, that would be 1235000.0.
@ftonumn sp idig;
! Round to an integer, and we have 1235000. Notice that this is
! exactly the digits we want to print (if we stick a decimal point
! after the first).
.DoPrint;
if (idig >= 10*pow10) {
! Rounding errors have left us outside the decimal range of
! [1.0, 10.0) where we should be. Adjust to the next higher
! exponent.
expo++;
@div idig 10 idig;
}
! Trim off trailing zeroes, as long as there's at least one digit
! after the decimal point. (Delete this stanza if you want to
! keep the trailing zeroes.)
while (prec > 1) {
@mod idig 10 sp;
@jnz sp ?DoneTrimming;
@div pow10 10 pow10;
@div idig 10 idig;
prec--;
}
.DoneTrimming;
for (ix=0 : ix<=prec : ix++) {
@div idig pow10 sp;
@mod sp 10 sp;
@streamnum sp;
if (ix == 0)
@streamchar '.';
@div pow10 10 pow10;
}
! Print the exponent. There are two conventions coded here: the
! programmatic ("1.0e+00") and the literary ("1.0 x 10^0").
#ifndef FLOAT_PROGRAMMING_EXPONENTS;
PrintMultiplicationSign();
@streamstr "10";
@streamchar $5E;
@streamnum expo;
#ifnot;
! Convention is to use at least two digits.
@streamchar 'e';
if (expo < 0) {
@streamchar '-';
@neg expo expo;
}
else {
@streamchar '+';
}
if (expo < 10)
@streamchar '0';
@streamnum expo;
#endif; ! FLOAT_PROGRAMMING_EXPONENTS
rtrue;
.IsNan;
PrintNan();
rtrue;
.IsInf;
PrintInfinity();
rtrue;
];
! Print a float in decimal notation: "[-]NNN.NNNNN".
! The precision is the number of digits after the decimal point
! (at least one, no more than eight). The default is five, because
! beyond that rounding errors creep in, and even exactly-represented
! float values are printed with trailing fudgy digits.
! Trailing zeroes are trimmed.
[ FloatDec val prec log10val int fint extra0 frac idig ix pow10;
if (prec == 0)
prec = 5;
if (prec > 8)
prec = 8;
pow10 = PowersOfTen --> prec;
! Knock off the sign bit first.
if (val & $80000000) {
@streamchar '-';
val = val & $7FFFFFFF;
}
@jisnan val ?IsNan;
@jisinf val ?IsInf;
! Take as an example val=123.5, with precision=6. The desired result
! is "123.50000".
extra0 = 0;
@fmod val $3F800000 frac fint; ! $3F800000 is 1.0.
@ftonumz fint int;
! This converts the integer part of the value to an integer value;
! in our example, 123.
if (int == $7FFFFFFF) {
! Looks like the integer part of the value is bigger than
! we can store in an int variable. (It could be as large
! as 3e+38.) We're going to have to use a log function to
! reduce it by some number of factors of 10, and then pad
! with zeroes.
@log fint sp;
@fdiv sp $40135D8E log10val; ! $40135D8E is log(10)
@ftonumz log10val extra0;
@sub extra0 8 extra0;
! extra0 is the number of zeroes we'll be padding with.
@numtof extra0 sp;
@fsub log10val sp sp;
@fmul sp $40135D8E sp;
@exp sp sp;
! The stack value is now exp((log10val - extra0) * log(10)).
! We've shifted the decimal point far enough left to leave
! about eight digits, which is all we can print as an integer.
@ftonumz sp int;
}
! Print the integer part.
@streamnum int;
for (ix=0 : ix<extra0 : ix++)
@streamchar '0';
@streamchar '.';
! Now we need to print the frac part, which is .5.
@log frac sp;
@fdiv sp $40135D8E log10val; ! $40135D8E is log(10)
@numtof prec sp;
@fadd log10val sp sp;
@fmul sp $40135D8E sp;
@exp sp sp;
! The stack value is now exp((frac + prec) * log(10)).
! We've shifted the decimal point right by prec
! digits. In our example, that would be 50000.0.
@ftonumn sp idig;
! Round to an integer, and we have 50000. Notice that this is
! exactly the (post-decimal-point) digits we want to print.
.DoPrint;
if (idig >= pow10) {
! Rounding errors have left us outside the decimal range of
! [0.0, 1.0) where we should be. I'm not sure this is possible,
! actually, but we'll just adjust downward.
idig = pow10 - 1;
}
! Trim off trailing zeroes, as long as there's at least one digit
! after the decimal point. (Delete this stanza if you want to
! keep the trailing zeroes.)
while (prec > 1) {
@mod idig 10 sp;
@jnz sp ?DoneTrimming;
@div pow10 10 pow10;
@div idig 10 idig;
prec--;
}
.DoneTrimming;
@div pow10 10 pow10;
for (ix=0 : ix<prec : ix++) {
@div idig pow10 sp;
@mod sp 10 sp;
@streamnum sp;
@div pow10 10 pow10;
}
rtrue;
.IsNan;
PrintNan();
rtrue;
.IsInf;
PrintInfinity();
rtrue;
];
[ PrintInfinity;
@streamunichar $221E;
! @streamstr "Inf";
];
[ PrintNan;
@streamunichar $26a0;
! @streamstr "NaN";
];
[ PrintMultiplicationSign;
print " ";
@streamunichar $D7;
print " ";
! @streamstr " x ";
];
#Ifnot; ! TARGET_GLULX
[ REAL_NUMBER_TY_Say real; print real; ]; ! Needs to exist, but likely never used
[ REAL_NUMBER_TY_Compare r1 r2; return UnsignedCompare(r1, r2); ];
#Endif; ! TARGET_GLULX