Skip to content

fix(js): keep Bytes.toHex character codes non-null - #13004

Closed
fullofcaffeine wants to merge 1 commit into
HaxeFoundation:developmentfrom
fullofcaffeine:fix/js-bytes-tohex-non-null
Closed

fix(js): keep Bytes.toHex character codes non-null#13004
fullofcaffeine wants to merge 1 commit into
HaxeFoundation:developmentfrom
fullofcaffeine:fix/js-bytes-tohex-non-null

Conversation

@fullofcaffeine

Copy link
Copy Markdown

Fixes #13003.

Why

The JavaScript standard library correctly types String.charCodeAt as
Null<Int> because an arbitrary index can be outside the string.

Bytes.toHex, however, reads the fixed hexadecimal alphabet only at indices
from 0 through str.length - 1:

var chars = [];
var str = "0123456789abcdef";

for (i in 0...str.length)
  chars.push(str.charCodeAt(i));

Every value added to this particular lookup table is therefore an Int, but
the unannotated array is inferred as Array<Null<Int>>.

That difference is invisible in ordinary JavaScript output because Haxe types
are erased. It matters to typed custom generators, though. The later
StringBuf.addChar(c:Int) and String.fromCharCode(code:Int) calls are inline,
so their Int parameter boundaries no longer exist in the final typed
expression. A strict TypeScript generator can consequently receive a nullable
array read inside raw JavaScript syntax:

const chars: (number | null)[] = [];

s_b += String.fromCodePoint(chars[c >> 4] ?? null);
s_b += String.fromCodePoint(chars[c & 15] ?? null);

TypeScript reports both arguments because number | null is not assignable to
the host API's number parameter.

What

Declare the lookup table's intended element type:

-var chars = [];
+var chars:Array<Int> = [];

How

The annotation records the local invariant where the table is constructed,
before inlining can erase later parameter boundaries.

A typed custom generator can now observe the ordinary Haxe call boundary:

  • source expression: Null<Int>
  • Array<Int>.push parameter: Int

For example, Genes renders that already-typed boundary as:

const chars: number[] = [];

chars.push(Register.unsafeCast<number>(HxOverrides.cca(str, i)));

s_b += String.fromCodePoint(chars[c >> 4]!);
s_b += String.fromCodePoint(chars[c & 15]!);

Register.unsafeCast<number> is an identity operation used by that generator:
it returns the same value once without conversion or validation. The important
part for Haxe is not the helper spelling; it is that the final typed tree still
contains the exact Null<Int>-to-Int call boundary instead of requiring a
generator to infer a type from raw JavaScript template text.

Classic JavaScript remains unchanged:

const chars = [];
chars.push(HxOverrides.cca(str, i));
s_b += String.fromCodePoint(chars[c >> 4]);
s_b += String.fromCodePoint(chars[c & 15]);

Tests

The new JavaScript unit test checks both parts of the contract:

  • a macro inspects the final typed Bytes.toHex field and requires the local
    lookup table to be Array<Int>;
  • a runtime vector verifies that bytes 00 0f 10 7f 80 ff produce
    000f107f80ff.

Validation performed:

  • proved the regression test fails as Array<Null<Int>> without the annotation
    and passes as Array<Int> with it;
  • ran the JavaScript unit suite: 11,439 assertions, 0 failures;
  • compiled and ran representative Bytes.toHex vectors with Haxe 4.3.7;
  • compared complete classic JavaScript outputs before and after the annotation:
    byte-identical SHA-256 hashes;
  • regenerated the package-neutral downstream strict-TypeScript fixture with a
    temporarily patched Haxe 4.3.7: exactly 16 diagnostics became exactly 14,
    removing only the two Bytes.toHex argument reports.

Scope

This does not change String.charCodeAt's general nullable contract, add raw
syntax recognition to a custom generator, or introduce a downstream standard
library override. It only states the stronger invariant already established by
this bounded loop.

Prepared by the GameCarry agent.

@RblSb

RblSb commented Jul 30, 2026

Copy link
Copy Markdown
Member

It can also use fastCodeAt to be faster and be typed as Int

@fullofcaffeine
fullofcaffeine marked this pull request as ready for review July 30, 2026 00:44
@fullofcaffeine fullofcaffeine closed this by deleting the head repository Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JS Bytes.toHex infers a nullable hexadecimal lookup table

2 participants