software-mansion / software-mansion/TypeGPU

(RFC) feat: Objects pretending to be primitives (branded numbers on the type level)

Open
#2,163 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
3.2k
Forks
122
Avg merge
3d 5h
Merged PRs (30d)
34

Description

Better docs

Standard (and user-defined) functions can have their proper accepted and return types defined clearly, available on hover.

Better view into the flow of types

With tsover enabled, d.f32(2) + d.f32(3.5) is no longer typed as number, it's typed as d.f32. Extrapolating this property, we can hover over any variable and know its proper WGSL type.

const main = () => {
  'use gpu';
  const c = d.f32(2) + d.f32(3.5);
  //    ^ d.f32
};

With tsover disabled, d.f32 can just be an alias for number

const main = () => {
  'use gpu';
  const c = d.f32(2) + d.f32(3.5);
  //    ^ number
};

Referentiality

Even though d.f32 is typed as number & { ... }, it's still an object at runtime. This allows us to hold more interesting metadata when executing a function in JS mode, but it also introduces a bit more complications.
One of them is that values returned by d.f32 are not primitives, therefore assigning the same value to multiple variables doesn't copy.

But perhaps this is not a problem if we keep these objects immutable. So what if two variables hold the same object, we know that the inner value will never change.

const main = () => {
  'use gpu';
  const a = d.f32(0);
  const b = a; // holds the same reference
};

typeof foo === 'number'

The primitives will be objects at runtime, meaning typeof foo will be 'object' and not 'number' as might be expected.
To fix this, we have to limit where these numeric objects can end up, while still making good use of them in shader functions.
My current idea for this is to transform typeof foo calls into __tsover_typeof__ and allow that operator to be overridable by tsover. I wouldn't want to patch up this behavior outside of 'use gpu' functions though, so to limit numeric objects to live only inside shader functions:

  1. Functions return numeric objects only when called from within another shader function, otherwise they're coerced back to vanilla numbers. This can be tracked by injecting globalThis.__TYPEGPU_ENTER_FN__(fn); try { /* the function code */ } finally { globalThis.__TYPEGPU_EXIT_FN__(fn); } into each 'use gpu' function body
  2. Numeric objects passed as arguments to comptime functions are reduced to vanilla numbers.

Reflection

Passing around objects instead of primitives has the added benefit of us being able to introspect the value's type and act on it at compile time:

const flip = (hello: d.f32 | d.f16): d.f32 | d.f16 => {
  'use gpu';
  if (std.typeOf(hello).type === 'f32') {
    return d.f16(hello);
  }
  return d.f32(hello);
};

Limitation

The limitation is that numeric literals aren't assignable to these branded types, and such have to be wrapped each time.

// Without tsover
function remap(a: f32, b: f32, value: f32): f32 {
  'use gpu';
  return f32((value - a) / (b - a));
}

// With tsover
function remap(a: f32, b: f32, value: f32): f32 {
  'use gpu';
  return (value - a) / (b - a);
}

function main() {
  const value = remap(f32(1), f32(2), f32(1.5));
  //  instead of: const value = remap(1, 2, 1.5);
}

We could also alias f32 to make it a bit easier on the eyes

function main() {
  const value = remap(f(1), f(2), f(1.5));
}

We could assume that number is f32 by default, but then for loops become very confusing

for (let i = 0; i < 10; i++) {
  // ...
}

becomes:

for (var i = 0f; i < 10f; i++) {
  // ...
}

This goes away however if we make for loops a special case. Worth investigating.

💡 What if we made strict numerals apply only with tsover?

Test out if using d.f32 can be made equivalent to just using number when tsover is disabled.
Then, unfortunately, it's not much of a guarantee when libraries say they expect say d.f32

💡 What if d.f32 were always branded types, but d.InferGPU would return different things depending on tsover's existance?

With tsover:

function remap(a: f32, b: f32, value: f32) {
  'use gpu';
  return (value - a) / (b - a);
}

const time = root.createUniform(f32);

function main() {
  'use gpu';
  const t = time.$; // => f32
  const factor = std.saturate(remap(f32(1), f32(2), t));
}

Without tsover:

function remap(a: f32, b: f32, value: f32) {
  'use gpu';
  return (value - a) / (b - a);
}

const time = root.createUniform(f32);

function main() {
  'use gpu';
  const t = time.$; // => number
  const factor = std.saturate(remap(f32(1), f32(2), d.f32(t)));
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reviewing the RFC's proposed branded d.f32 types, tsover behavior, runtime referentiality, typeof handling, and numeric-literal limitations. Done would require an agreed design for these trade-offs and a concrete implementation plan identifying the affected compiler and runtime entry points.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.