Implement opt Q for fixed-point precision, and q literals (e.g. 12.34q8) (#958)

Fixes #957

Co-authored-by: ISSOtm <eldredhabert0@gmail.com>
This commit is contained in:
Rangi
2022-09-04 18:47:32 -04:00
committed by GitHub
parent 889302a9e2
commit 98a6dffbca
20 changed files with 253 additions and 56 deletions

View File

@@ -39,6 +39,7 @@ _rgbasm_completions() {
[M]="dependfile:glob-*.mk *.d"
[o]="output:glob-*.o"
[p]="pad-value:unk"
[Q]="q-precision:unk"
[r]="recursion-depth:unk"
[W]="warning:warning"
)

View File

@@ -56,6 +56,7 @@ local args=(
'*'-MQ"+[Add a target to the rules]:target:_files -g '*.{d,mk,o}'"
'(-o --output)'{-o,--output}'+[Output file]:output file:_files'
'(-p --pad-value)'{-p,--pad-value}'+[Set padding byte]:padding byte:'
'(-Q --q-precision)'{-Q,--q-precision}'+[Set fixed-point precision]:precision:'
'(-r --recursion-depth)'{-r,--recursion-depth}'+[Set maximum recursion depth]:depth:'
'(-W --warning)'{-W,--warning}'+[Toggle warning flags]:warning flag:_rgbasm_warnings'

View File

@@ -11,6 +11,9 @@
#include <stdint.h>
extern uint8_t fixPrecision;
double fix_PrecisionFactor(void);
void fix_Print(int32_t i);
int32_t fix_Sin(int32_t i);
int32_t fix_Cos(int32_t i);

View File

@@ -14,7 +14,8 @@
void opt_B(char const chars[2]);
void opt_G(char const chars[4]);
void opt_P(uint8_t fill);
void opt_P(uint8_t padByte);
void opt_Q(uint8_t precision);
void opt_L(bool optimize);
void opt_W(char const *flag);
void opt_Parse(char const *option);

View File

@@ -25,6 +25,7 @@
.Op Fl MQ Ar target_file
.Op Fl o Ar out_file
.Op Fl p Ar pad_value
.Op Fl Q Ar fix_precision
.Op Fl r Ar recursion_depth
.Op Fl W Ar warning
.Ar
@@ -148,6 +149,13 @@ Write an object file to the given filename.
.It Fl p Ar pad_value , Fl Fl pad-value Ar pad_value
When padding an image, pad with this value.
The default is 0x00.
.It Fl Q Ar fix_precision , Fl Fl q-precision Ar fix_precision
Use this as the precision of fixed-point numbers after the decimal point, unless they specify their own precision.
The default is 16, so fixed-point numbers are Q16.16 (since they are 32-bit integers).
The argument may start with a
.Ql \&.
to match the Q notation, for example,
.Ql Fl Q Ar .16 .
.It Fl r Ar recursion_depth , Fl Fl recursion-depth Ar recursion_depth
Specifies the recursion depth past which RGBASM will assume being in an infinite loop.
The default is 64.

View File

@@ -208,13 +208,14 @@ section.
The instructions in the macro-language generally require constant expressions.
.Ss Numeric formats
There are a number of numeric formats.
.Bl -column -offset indent "Fixed point (Q16.16)" "Prefix"
.Bl -column -offset indent "Precise fixed-point" "Prefix"
.It Sy Format type Ta Sy Prefix Ta Sy Accepted characters
.It Hexadecimal Ta $ Ta 0123456789ABCDEF
.It Decimal Ta none Ta 0123456789
.It Octal Ta & Ta 01234567
.It Binary Ta % Ta 01
.It Fixed point (Q16.16) Ta none Ta 01234.56789
.It Fixed-point Ta none Ta 01234.56789
.It Precise fixed-point Ta none Ta 12.34q8
.It Character constant Ta none Ta \(dqABYZ\(dq
.It Gameboy graphics Ta \` Ta 0123
.El
@@ -301,9 +302,19 @@ and
.Ic \&!
returns 1 if the operand was 0, and 0 otherwise.
.Ss Fixed-point expressions
Fixed-point numbers are basically normal (32-bit) integers, which count 65536ths instead of entire units, offering better precision than integers but limiting the range of values.
The upper 16 bits are used for the integer part and the lower 16 bits are used for the fraction (65536ths).
Since they are still akin to integers, you can use them in normal integer expressions, and some integer operators like
Fixed-point numbers are basically normal (32-bit) integers, which count fractions instead of whole numbers.
They offer better precision than integers but limit the range of values.
By default, the upper 16 bits are used for the integer part and the lower 16 bits are used for the fraction (65536ths).
The default number of fractional bits can be changed with the
.Fl Q
command-line option.
You can also specify a precise fixed-point value by appending a
.Dq q
to it followed by the number of fractional bits, such as
.Ql 12.34q8 .
.Pp
Since fixed-point values are still just integers, you can use them in normal integer expressions.
Some integer operators like
.Sq +
and
.Sq -
@@ -317,9 +328,9 @@ delim $$
.EN
.Bl -column -offset indent "ATAN2(x, y)"
.It Sy Name Ta Sy Operation
.It Fn DIV x y Ta $x \[di] y$
.It Fn MUL x y Ta $x \[mu] y$
.It Fn FMOD x y Ta $x % y$
.It Fn DIV x y Ta Fixed-point division $( x \[di] y ) \[mu] ( 2 ^ precision )$
.It Fn MUL x y Ta Fixed-point multiplication $( x \[mu] y ) \[di] ( 2 ^ precision )$
.It Fn FMOD x y Ta Fixed-point modulo $( x % y ) \[di] ( 2 ^ precision )$
.It Fn POW x y Ta $x$ to the $y$ power
.It Fn LOG x y Ta Logarithm of $x$ to the base $y$
.It Fn ROUND x Ta Round $x$ to the nearest integer
@@ -2034,7 +2045,7 @@ POPO
The options that
.Ic OPT
can modify are currently:
.Cm b , g , p , r , h , L ,
.Cm b , g , p , Q , r , h , L ,
and
.Cm W .
The Boolean flag options

View File

@@ -17,17 +17,24 @@
#include "asm/symbol.h"
#include "asm/warning.h"
#define fix2double(i) ((double)((i) / 65536.0))
#define double2fix(d) ((int32_t)round((d) * 65536.0))
// pi radians == 32768 fixed-point "degrees"
#define fdeg2rad(f) ((f) * (M_PI / 32768.0))
#define rad2fdeg(r) ((r) * (32768.0 / M_PI))
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#define fix2double(i) ((double)((i) / fix_PrecisionFactor()))
#define double2fix(d) ((int32_t)round((d) * fix_PrecisionFactor()))
// pi*2 radians == 2**fixPrecision fixed-point "degrees"
#define fdeg2rad(f) ((f) * (M_PI * 2) / fix_PrecisionFactor())
#define rad2fdeg(r) ((r) * fix_PrecisionFactor() / (M_PI * 2))
uint8_t fixPrecision;
double fix_PrecisionFactor(void)
{
return pow(2.0, fixPrecision);
}
void fix_Print(int32_t i)
{
uint32_t u = i;
@@ -38,7 +45,7 @@ void fix_Print(int32_t i)
sign = "-";
}
printf("%s%" PRIu32 ".%05" PRIu32, sign, u >> 16,
printf("%s%" PRIu32 ".%05" PRIu32, sign, u >> fixPrecision,
((uint32_t)(fix2double(u) * 100000 + 0.5)) % 100000);
}

View File

@@ -15,6 +15,7 @@
#include <stdlib.h>
#include <string.h>
#include "asm/fixpoint.h"
#include "asm/format.h"
#include "asm/warning.h"
@@ -225,7 +226,7 @@ void fmt_PrintNumber(char *buf, size_t bufLen, struct FormatSpec const *fmt, uin
} else if (fmt->type == 'f') {
// Special case for fixed-point
// Default fractional width (C's is 6 for "%f"; here 5 is enough)
// Default fractional width (C's is 6 for "%f"; here 5 is enough for Q16.16)
size_t fracWidth = fmt->hasFrac ? fmt->fracWidth : 5;
if (fracWidth > 255) {
@@ -234,7 +235,8 @@ void fmt_PrintNumber(char *buf, size_t bufLen, struct FormatSpec const *fmt, uin
fracWidth = 255;
}
snprintf(valueBuf, sizeof(valueBuf), "%.*f", (int)fracWidth, value / 65536.0);
snprintf(valueBuf, sizeof(valueBuf), "%.*f", (int)fracWidth,
value / fix_PrecisionFactor());
} else {
char const *spec = fmt->type == 'd' ? "%" PRId32
: fmt->type == 'u' ? "%" PRIu32

View File

@@ -27,6 +27,7 @@
#include "platform.h" // For `ssize_t`
#include "asm/lexer.h"
#include "asm/fixpoint.h"
#include "asm/format.h"
#include "asm/fstack.h"
#include "asm/macro.h"
@@ -1125,17 +1126,28 @@ static uint32_t readNumber(int radix, uint32_t baseValue)
return value;
}
static uint32_t readFractionalPart(int32_t integer)
static uint32_t readFractionalPart(uint32_t integer)
{
uint32_t value = 0, divisor = 1;
uint8_t precision = 0;
enum {
READFRACTIONALPART_DIGITS,
READFRACTIONALPART_PRECISION,
READFRACTIONALPART_PRECISION_DIGITS,
} state = READFRACTIONALPART_DIGITS;
for (;; shiftChar()) {
int c = peek();
if (c == '_')
if (state == READFRACTIONALPART_DIGITS) {
if (c == '_') {
continue;
else if (c < '0' || c > '9')
} else if (c == 'q' || c == 'Q') {
state = READFRACTIONALPART_PRECISION;
continue;
} else if (c < '0' || c > '9') {
break;
}
if (divisor > (UINT32_MAX - (c - '0')) / 10) {
warning(WARNING_LARGE_CONSTANT,
"Precision of fixed-point constant is too large\n");
@@ -1147,17 +1159,33 @@ static uint32_t readFractionalPart(int32_t integer)
}
value = value * 10 + (c - '0');
divisor *= 10;
} else {
if (c == '.' && state == READFRACTIONALPART_PRECISION) {
state = READFRACTIONALPART_PRECISION_DIGITS;
continue;
} else if (c < '0' || c > '9') {
break;
}
precision = precision * 10 + (c - '0');
}
}
if (integer > INT16_MAX || integer < INT16_MIN)
if (precision == 0) {
if (state >= READFRACTIONALPART_PRECISION)
error("Invalid fixed-point constant, no significant digits after 'q'\n");
precision = fixPrecision;
} else if (precision > 31) {
error("Fixed-point constant precision must be between 1 and 31\n");
precision = fixPrecision;
}
if (integer >= ((uint32_t)1 << (precision - 1)))
warning(WARNING_LARGE_CONSTANT, "Magnitude of fixed-point constant is too large\n");
// Cast to unsigned avoids UB if shifting discards bits
integer = (uint32_t)integer << 16;
// Cast to unsigned avoids undefined overflow behavior
uint16_t fractional = (uint16_t)round(value * 65536.0 / divisor);
uint32_t fractional = (uint32_t)round((double)value / divisor * pow(2.0, precision));
return (uint32_t)integer | (fractional * (integer >= 0 ? 1 : -1));
return (integer << precision) | fractional;
}
char binDigits[2];
@@ -1741,6 +1769,8 @@ static int yylex_SKIP_TO_ENDC(void); // forward declaration for yylex_NORMAL
static int yylex_NORMAL(void)
{
uint32_t num = 0;
for (;;) {
int c = nextChar();
char secondChar;
@@ -1912,10 +1942,12 @@ static int yylex_NORMAL(void)
case '7':
case '8':
case '9':
yylval.constValue = readNumber(10, c - '0');
num = readNumber(10, c - '0');
if (peek() == '.') {
shiftChar();
yylval.constValue = readFractionalPart(yylval.constValue);
yylval.constValue = readFractionalPart(num);
} else {
yylval.constValue = num;
}
return T_NUMBER;

View File

@@ -19,6 +19,7 @@
#include <time.h>
#include "asm/charmap.h"
#include "asm/fixpoint.h"
#include "asm/format.h"
#include "asm/fstack.h"
#include "asm/lexer.h"
@@ -86,7 +87,7 @@ static char *make_escape(char const *str)
}
// Short options
static const char *optstring = "b:D:Eg:Hhi:LlM:o:p:r:VvW:w";
static const char *optstring = "b:D:Eg:Hhi:LlM:o:p:Q:r:VvW:w";
// Variables for the long-only options
static int depType; // Variants of `-M`
@@ -116,6 +117,7 @@ static struct option const longopts[] = {
{ "MQ", required_argument, &depType, 'Q' },
{ "output", required_argument, NULL, 'o' },
{ "pad-value", required_argument, NULL, 'p' },
{ "q-precision", required_argument, NULL, 'Q' },
{ "recursion-depth", required_argument, NULL, 'r' },
{ "version", no_argument, NULL, 'V' },
{ "verbose", no_argument, NULL, 'v' },
@@ -128,7 +130,8 @@ static void print_usage(void)
fputs(
"Usage: rgbasm [-EHhLlVvw] [-b chars] [-D name[=value]] [-g chars] [-i path]\n"
" [-M depend_file] [-MG] [-MP] [-MT target_file] [-MQ target_file]\n"
" [-o out_file] [-p pad_value] [-r depth] [-W warning] <file>\n"
" [-o out_file] [-p pad_value] [-Q precision] [-r depth]\n"
" [-W warning] <file>\n"
"Useful options:\n"
" -E, --export-all export all labels\n"
" -M, --dependfile <path> set the output dependency file\n"
@@ -170,6 +173,7 @@ int main(int argc, char *argv[])
opt_B("01");
opt_G("0123");
opt_P(0);
opt_Q(16);
haltnop = true;
warnOnHaltNop = true;
optimizeLoads = true;
@@ -250,17 +254,34 @@ int main(int argc, char *argv[])
out_SetFileName(musl_optarg);
break;
unsigned long fill;
unsigned long padByte;
case 'p':
fill = strtoul(musl_optarg, &ep, 0);
padByte = strtoul(musl_optarg, &ep, 0);
if (musl_optarg[0] == '\0' || *ep != '\0')
errx("Invalid argument for option 'p'");
if (fill > 0xFF)
if (padByte > 0xFF)
errx("Argument for option 'p' must be between 0 and 0xFF");
opt_P(fill);
opt_P(padByte);
break;
unsigned long precision;
const char *precisionArg;
case 'Q':
precisionArg = musl_optarg;
if (precisionArg[0] == '.')
precisionArg++;
precision = strtoul(precisionArg, &ep, 0);
if (musl_optarg[0] == '\0' || *ep != '\0')
errx("Invalid argument for option 'Q'");
if (precision < 1 || precision > 31)
errx("Argument for option 'Q' must be between 1 and 31");
opt_Q(precision);
break;
case 'r':

View File

@@ -14,6 +14,7 @@
#include <stdlib.h>
#include <string.h>
#include "asm/fixpoint.h"
#include "asm/fstack.h"
#include "asm/lexer.h"
#include "asm/main.h"
@@ -23,7 +24,8 @@
struct OptStackEntry {
char binary[2];
char gbgfx[4];
int32_t fillByte;
uint8_t fixPrecision;
uint8_t fillByte;
bool haltnop;
bool warnOnHaltNop;
bool optimizeLoads;
@@ -47,9 +49,14 @@ void opt_G(char const chars[4])
lexer_SetGfxDigits(chars);
}
void opt_P(uint8_t fill)
void opt_P(uint8_t padByte)
{
fillByte = fill;
fillByte = padByte;
}
void opt_Q(uint8_t precision)
{
fixPrecision = precision;
}
void opt_R(size_t newDepth)
@@ -103,18 +110,41 @@ void opt_Parse(char *s)
case 'p':
if (strlen(&s[1]) <= 2) {
int result;
unsigned int fillchar;
unsigned int padByte;
result = sscanf(&s[1], "%x", &fillchar);
if (result != EOF && result != 1)
result = sscanf(&s[1], "%x", &padByte);
if (result != 1)
error("Invalid argument for option 'p'\n");
else if (padByte > 0xFF)
error("Argument for option 'p' must be between 0 and 0xFF\n");
else
opt_P(fillchar);
opt_P(padByte);
} else {
error("Invalid argument for option 'p'\n");
}
break;
const char *precisionArg;
case 'Q':
precisionArg = &s[1];
if (precisionArg[0] == '.')
precisionArg++;
if (strlen(precisionArg) <= 2) {
int result;
unsigned int precision;
result = sscanf(precisionArg, "%u", &precision);
if (result != 1)
error("Invalid argument for option 'Q'\n");
else if (precision < 1 || precision > 31)
error("Argument for option 'Q' must be between 1 and 31\n");
else
opt_Q(precision);
} else {
error("Invalid argument for option 'Q'\n");
}
break;
case 'r': {
++s; // Skip 'r'
while (isblank(*s))
@@ -231,6 +261,8 @@ void opt_Push(void)
entry->gbgfx[2] = gfxDigits[2];
entry->gbgfx[3] = gfxDigits[3];
entry->fixPrecision = fixPrecision; // Pulled from fixpoint.h
entry->fillByte = fillByte; // Pulled from section.h
entry->haltnop = haltnop; // Pulled from main.h
@@ -259,6 +291,7 @@ void opt_Pop(void)
opt_B(entry->binary);
opt_G(entry->gbgfx);
opt_P(entry->fillByte);
opt_Q(entry->fixPrecision);
opt_H(entry->warnOnHaltNop);
opt_h(entry->haltnop);
opt_L(entry->optimizeLoads);

View File

@@ -12,3 +12,11 @@ fl = 6.283185
fr = MUL(20.0, 0.32)
println "32% of 20 = {f:fr} (~{.2f:fr}) (~~{.0f:fr})"
q8 = 1.25q8
q16 = 1.25Q16
q24 = 1.25q.24
println "Q8 ${x:q8} Q16 ${x:q16} Q24 ${x:q24}"
qerr = 1.25q32
println qerr

View File

@@ -0,0 +1,3 @@
error: fixed-point-precision.asm(21):
Fixed-point constant precision must be between 1 and 31
error: Assembly aborted (1 error)!

View File

@@ -4,3 +4,5 @@
`16.12`: 16.119995 -> $00101eb8
`6.283185`: 6.283188 -> $0006487f
32% of 20 = 6.40015 (~6.40) (~~6)
Q8 $140 Q16 $14000 Q24 $1400000
$14000

18
test/asm/opt-Q.asm Normal file
View File

@@ -0,0 +1,18 @@
MACRO test
PUSHO
OPT Q\1
print STRFMT("Q%4s", "\1")
def n = 1.14159
println STRFMT(" -> %032b", n)
POPO
ENDM
for x, 1, 32
if x < 16
test .{d:x}
else
test {d:x}
endc
endr
test .0 ; error
test 32 ; error

7
test/asm/opt-Q.err Normal file
View File

@@ -0,0 +1,7 @@
warning: opt-Q.asm(10) -> opt-Q.asm::REPT~1(12) -> opt-Q.asm::test(5): [-Wlarge-constant]
Magnitude of fixed-point constant is too large
error: opt-Q.asm(17) -> opt-Q.asm::test(3):
Argument for option 'Q' must be between 1 and 31
error: opt-Q.asm(18) -> opt-Q.asm::test(3):
Argument for option 'Q' must be between 1 and 31
error: Assembly aborted (2 errors)!

33
test/asm/opt-Q.out Normal file
View File

@@ -0,0 +1,33 @@
Q .1 -> 00000000000000000000000000000010
Q .2 -> 00000000000000000000000000000101
Q .3 -> 00000000000000000000000000001001
Q .4 -> 00000000000000000000000000010010
Q .5 -> 00000000000000000000000000100101
Q .6 -> 00000000000000000000000001001001
Q .7 -> 00000000000000000000000010010010
Q .8 -> 00000000000000000000000100100100
Q .9 -> 00000000000000000000001001001000
Q .10 -> 00000000000000000000010010010001
Q .11 -> 00000000000000000000100100100010
Q .12 -> 00000000000000000001001001000100
Q .13 -> 00000000000000000010010010001000
Q .14 -> 00000000000000000100100100010000
Q .15 -> 00000000000000001001001000100000
Q 16 -> 00000000000000010010010000111111
Q 17 -> 00000000000000100100100001111110
Q 18 -> 00000000000001001001000011111101
Q 19 -> 00000000000010010010000111111010
Q 20 -> 00000000000100100100001111110100
Q 21 -> 00000000001001001000011111101000
Q 22 -> 00000000010010010000111111010000
Q 23 -> 00000000100100100001111110011111
Q 24 -> 00000001001001000011111100111110
Q 25 -> 00000010010010000111111001111100
Q 26 -> 00000100100100001111110011111000
Q 27 -> 00001001001000011111100111110000
Q 28 -> 00010010010000111111001111100000
Q 29 -> 00100100100001111110011111000000
Q 30 -> 01001001000011111100111110000001
Q 31 -> 10010010000111111001111100000010
Q .0 -> 00000000000000010010010000111111
Q 32 -> 00000000000000010010010000111111

View File

@@ -3,11 +3,13 @@ SECTION "test", ROM0
opt !h, !L ; already the default, but tests parsing "!"
pusho
opt p42, h, L, Wno-div
opt p42, Q.4, h, L, Wno-div
ds 1
ld [$ff88], a
halt
println $8000_0000 / -1
def n = 3.14
println "{x:n} = {f:n}"
popo
opt H, l
@@ -16,3 +18,5 @@ popo
ld [$ff88], a
halt
println $8000_0000 / -1
def n = 3.14
println "{x:n} = {f:n}"

View File

@@ -1,2 +1,2 @@
warning: opt.asm(18): [-Wdiv]
warning: opt.asm(20): [-Wdiv]
Division of -2147483648 by -1 yields -2147483648

View File

@@ -1,2 +1,4 @@
$80000000
32 = 3.12500
$80000000
323d7 = 3.14000