fusionlanguage / fut

Fusion programming language. Transpiling to C, C++, C#, D, Java, JavaScript, Python, Swift, TypeScript and OpenCL C.
https://fusion-lang.org
GNU General Public License v3.0
1.74k stars 55 forks source link

improvements to float32 support needed #172

Open teadrinker opened 1 month ago

teadrinker commented 1 month ago

There seems to be no way to define a float inline (such as 1f or 1.f) There is also no way to do type cast as far as I can tell (if there is, please add to documentation) It's also a bit unclear how the auto-casting behaves, if you assign to a float, it always seem to make sure type is correct, but if you pass a float to Math.Sin, it does not (this actually causes compile error on c#)

float a = 1.0;
float b = Math.Sin(a);

However, For C/C#, ideally you want sinf/MathF if you pass a float to sin, and not to have it converted to double... I guess it works like that automatically in C++ already since std::sin has both versions... (I would also be fine with a separate prefix, so the fusion code would be explicit in which version you call, just like in c#)

(https://github.com/fusionlanguage/fut/issues/83 seem to be a related issue)

teadrinker commented 1 month ago

I saw casting was mentioned for integers using Math.Trunc (however, this function seem to have been renamed Truncate now)

It feels a bit weird that it currently maps to js/ts trunc which is more like floor in the way that it supports values outside the integer range, I feel it would be more consistent to map it to (x) => x|0

Anyway, glad to find this cool project! Super impressive to support all those container types, congrats! :)

pfusik commented 1 month ago

Thank you for kind words!

There are no float literals. Every float literal can be precisely expressed as a double literal. JavaScript doesn't have floats at all, so you shouldn't rely on the decreased precision. Use float solely to conserve memory.

There are no number cast operators. If you want a number of a different type, assign it to a new variable. float b = Math.Sin(a); is the correct syntax. I will fix the bug of translating it incorrectly. I will also emit the float math functions.

The result of Math.Truncate can be assigned to an integer. Thanks for spotting a doc typo. x | 0 is limited to 32-bit range, i.e. can't be used for longs. I will consider it for 32-bit integers.

teadrinker commented 1 month ago

Use float solely to conserve memory.

I see, I was thinking about cases like audio and graphics (using float for better performance) But I can see how encouraging float32 use would make the result less consistent between platforms... You can still do it of course, it's just the the code becomes longer/annoying when every constant need to have it's own variable in order not to use double.

I will consider it for 32-bit integers.

Sorry, yes, I realize now it is only relevant for the 32-bit case. (I thought python trunc returned an int32, however, it seems "int" is 64bit in python these days!)

pfusik commented 1 month ago

Indeed, for heavy calculations, float vs double makes a difference.

I intended the Math functions to be overloaded:

double d = Math.Sin(doubleAngle);
float f = Math.Sin(floatAngle);

The above are unambiguous and it's possible to transpile the second one to MathF. But what about:

double x = Math.Sin(floatAngle); // Math.Sin or MathF.Sin?
float y = 5 * Math.Sin(floatAngle); // float multiplication or double multiplication?

Now I think that adopting .NET's explicit MathF might be a good idea.

(I thought python trunc returned an int32, however, it seems "int" is 64bit in python these days!)

Not sure what you're referring to, but Python has bignums:

C:\0>python
Python 3.11.9 (main, Apr 12 2024, 09:55:31)  [GCC 13.2.0 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> int(1 << 2000)
114813069527425452423283320117768198402231770208869520047764273682576626139237031385665948631650626991844596463898746277344711896086305533142593135616665318539129989145312280000688779148240044871428926990063486244781615463646388363947317026040466353970904996558162398808944629605623311649536164221970332681344168908984458505602379484807914058900934776500429002716706625830522008132236281291761267883317206598995396418127021779858404042159853183251540889433902091920554957783589672039160081957216630582755380425583726015528348786419432054508915275783882625175435528800822842770817965453762184851149029376
>>> import math
>>> math.trunc(1 << 2000)
114813069527425452423283320117768198402231770208869520047764273682576626139237031385665948631650626991844596463898746277344711896086305533142593135616665318539129989145312280000688779148240044871428926990063486244781615463646388363947317026040466353970904996558162398808944629605623311649536164221970332681344168908984458505602379484807914058900934776500429002716706625830522008132236281291761267883317206598995396418127021779858404042159853183251540889433902091920554957783589672039160081957216630582755380425583726015528348786419432054508915275783882625175435528800822842770817965453762184851149029376
teadrinker commented 1 month ago

Nice, being explicit is probably the best way, will be easier to reason about the code.

Not sure what you're referring to, but Python has bignums

wow, that's sweet! I knew it had bignums, I never realized it's THE native int type! Guess porting/running hashing functions is not ideal then, but it's a pretty epic feat that range of native integers are only limited by memory!

pfusik commented 1 month ago

I keep thinking of the API design.

I expect the calculations to consistently use double for precision, or float for efficiency. No language provides functions with mixed parameter/result precisions. Hence:

double x = Math.Sin(floatAngle); // MathF.Sin
float y = 5 * Math.Sin(floatAngle); // float multiplication

In rare cases where precision needs to be mixed, intermediate variables can be introduced.

I'm going to fully read the MathF story, but at first it looks the reasons were backward compatibility (Math.Sin(2.0f) yielded a double in the already-popular .NET). MathF.Abs/Min/Max are duplicates of the corresponding Math methods. Not a good design. Especially if we later add half or whatever-new-fp-type.

.NET 7 adds float.Sin(foo) etc. Some languages go even further: foo.Sin().

teadrinker commented 1 month ago

I agree that we can assume mixed precision is not a common/important case

MathF is a pretty ugly prefix, Unity game engine has its own Mathf as well, so there is already at least 3 in Unity...

float.Sin(foo)

hmm, never saw that, but quite nice, possibly the best so far?

foo.Sin()

hmm, this would take some time to get use to, sin with empty parens, weird... I feel in many cases you'd get something like (a*b+c).Sin() anyway, which is just more complicated...

My personal preference is like C-style global sin(), but accepting multiple types like hlsl/glsl. However I think this is ruled out in this context due to naming conventions, that might also have issues with ambiguity (might still require something like float.Sin()/MathF.Sin() as fallback)

pfusik commented 1 month ago

I decided to add float overloads. float b = Math.Sin(a); works now, emitting sinf, MathF.Sin, Float sin overload in Swift, (float) Math.sin in Java.

Being explicit about the type is not how computations generally work in high-level languages. We are used to overloaded arithmetic operators. I see no reason to spell MathF or float for built-in math functions. And I agree the method syntax would be bizarre.

Not closing this ticket yet, as tests need to be added and Abs/Clamp/Max/Min need to be handled.

teadrinker commented 1 month ago

Being explicit about the type is not how computations generally work in high-level languages. We are used to overloaded arithmetic operators. I see no reason to spell MathF or float for built-in math functions.

I agree it's much more elegant if this can be solved using overloading!

Let me explain more about the context why I would like to see float literals supported... One of the things I've been doing somewhat regularly for 15 years is prototyping sound synthesis/processing inside js. The result is then manually converted to C/C++, or similar...

So the use case here is not really high level, nor a complete app, I would run fusion to emit js during prototyping, and then export a high performant C.

Example:

public double sweep(double x)
{
    double sweep = (1.0544 - Math.Cos(x / 4));
    return Math.Sin(42 * sweep * (1 + (3 + Math.Sin(x * 350)) / 8));
}
public float sweepf(float x)
{
    float f1 = 1.0;
    float f3 = 3.0;
    float f4 = 4.0;
    float f8 = 8.0;
    float f42 = 42.0;
    float f350 = 350.0;
    float c = 1.0544;
    float sweep = (c - Math.Cos(x / f4));
    return Math.Sin(f42 * sweep * (f1 + (f3 + Math.Sin(x * f350)) / f8));
}

so yeah, a very extreme example, and a very fringe use case in the grand scheme of things... So you might be better off ignoring this unless these are easy fixes...

An alternative approach that would work for this use case, is some kind of way to define the "default number precision", feels a bit more high level...

pfusik commented 1 month ago

Integer literals are promoted to float, not to double. So, with the current Git fut, this works with all floats:

public static float Sweep(float x)
{
    float offset = 1.0544;
    float sweep = offset - Math.Cos(x / 4);
    return Math.Sin(42 * sweep * (1 + (3 + Math.Sin(x * 350)) / 8));
}