Skip to content

v0.2.3

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 31 Aug 20:37
· 45 commits to main since this release
v0.2.3
dd6b32c

Added

  • Software binary floating point in rucc-base, with the six formats the compiler has to produce: binary16, bfloat16, binary32, binary64, the x87 eighty bit format with its stored leading significand bit, and binary128. A compiler cannot ask the machine it runs on what a floating constant means, because the host may not have the format at all, long double is eighty bits on x86-64 and a hundred and twenty eight on AArch64 Linux and sixty four on Apple, and strtod is the host's libc rather than the target's semantics. Reproducible output means the same source gives the same bits whoever compiles it, so the conversion is done here in integer arithmetic. It is correctly rounded, round to nearest with ties to even, using an exact decimal that is scaled by powers of two until the value is in the range the significand can be read off, which is the algorithm Go's strconv uses and the one Rust's own parser falls back to. A naive mantissa * 10^exponent in double precision is wrong in the last place for a noticeable fraction of literals, and the last place is exactly what a differential test against another compiler notices. The binary64 and binary32 answers are checked against Rust's own correctly rounded parser over four thousand generated numbers as well as the hard cases, including the seven hundred and sixty seven digit halfway value that a conversion truncating its input gets wrong, and the x87 and binary128 answers were measured by compiling the constants with gcc 13.3 and reading the bytes back out of the program. Hexadecimal constants are exact by construction and are rounded once at the end. Arithmetic is not here yet, since a constant does not need it; it comes with the constant evaluator.

  • Floating constants in rucc-lex, the other half of what a preprocessing number can turn into. A floating constant has none of the table walk an integer one has, because the suffix names the type outright, and what it has instead is a suffix list far longer than the standard's three and a conversion that has to be right to the last bit. The suffixes were measured on gcc 13.3 rather than recalled: q is __float128, w is the x87 __float80, d is a double written the long way, f16 through f128 are the _FloatN types, f32x and f64x the _FloatNx ones, and an i or a j on either side of the type makes the constant imaginary. Two of the answers are not what the names suggest: _Float32x is plain double and _Float64x is the x87 format, so 0.1f64x and 0.1l are the same bits on x86-64 Linux. The case rules are their own small grammar, since the f of a _FloatN suffix may be either case and the trailing x may not, so F64x is a constant and f64X is not, and the two letters of a decimal float suffix have to agree, so dd and DD are constants and dD is not. Every extension suffix is accepted in every dialect including C89, with a remark for the caller holding the span, which is what gcc does. Decimal floating constants are recognised and refused with an error that says so, because there is no decimal floating value anywhere in this compiler to put one in yet. A constant too large for its type becomes an infinity and one too small becomes a zero, both with a remark, which is the pair of warnings gcc gives.

  • Character constants and string literals in rucc-lex, which is the last piece of phase 7 that is about spellings and the first one whose answer depends on the target in a way a reader would not guess. A literal arrives as the bytes the user wrote and leaves as elements, and what an element is comes from the encoding prefix and from wchar_t, so L"a" followed by an emoji is two elements on Linux and three on Windows, where a wide string is UTF-16 and the character needs a surrogate pair. The escapes divide into two kinds and the division is the whole design: an escape that names a character gets encoded in the literal's encoding, so a plain "e-acute" is the two bytes c3 a9, and an escape that writes a value is that element as written and is truncated to the element with a remark when it does not fit, so '\x1ff' is minus one and L'\x1ff' is five hundred and eleven. Octal stops after three digits and hexadecimal runs as far as the digits go, which is why "\1234" is two characters and "\x41z" is two as well. A plain character constant is an int and its single character is converted through plain char first, so '\xff' is minus one on x86-64 Linux and two hundred and fifty five on AArch64, where plain char is unsigned. More than one character shifts them together and the ones past the width of the type fall off the front, so 'abcde' is 0x62636465, and gcc reports that case as too long instead of as multi-character rather than as well as it, which is a distinction only measurement gives you. Every value here was measured against gcc 13.3 on x86-64 Linux rather than recalled. There is one deliberate divergence: a universal character name above the end of Unicode is refused here and in clang, where gcc warns and encodes the value as though UTF-8 went that far.

  • wchar_width and wchar_is_signed in rucc-target, which is where the literals get their answer. The two fields split the targets in different directions and neither follows from anything else already there: the width is sixteen on Windows and thirty two everywhere else, and the sign follows the psABI's rule for plain char, so wchar_t is a signed int on x86-64 Linux and an unsigned int on AArch64 Linux and L'\xffffffff' is minus one on one and four billion on the other. The predefined macros in rucc-pp now derive __WCHAR_TYPE__, __WCHAR_MAX__, __WCHAR_MIN__ and __SIZEOF_WCHAR_T__ from these two fields rather than matching on the triple a second time, so the lexer and the macros cannot come to different conclusions about the same target.

  • long_double_format in rucc-target, because the width of long double does not say what it is. It is a hundred and twenty eight bits wide on x86-64 Linux and on AArch64 Linux and those are not the same type: one is the x87 eighty bit format padded out to sixteen bytes and the other is true quad precision with a hundred and thirteen bits of significand, so 1.0l is 0x3fff8000000000000000 on one and 0x3fff0000000000000000000000000000 on the other. Anything that converts a constant or folds one has to know which, and until now nothing could tell.

What's Changed

  • Add software binary floating point by @tamnd in #59
  • Add floating constants to the lexer by @tamnd in #60
  • Add character constants and string literals to the lexer by @tamnd in #61
  • Release 0.2.3 by @tamnd in #62

Full Changelog: v0.2.2...v0.2.3