Skip to content

@native-sdk/core/text fails the subset checker's integer proofs once TextEditState is a Model field #406

Description

@stewartjarod

Summary

applyTextInputEvent is the documented way to drive a markup text control from a TypeScript core: hold a TextEditState in the Model, feed it TextInputEvents, store the result. Doing exactly that fails native build with 19 integer-proof errors across 10 lines, all inside sdk/text.ts itself. The app's own code is never named.

native check passes. Only native build fails.

Reproduced against native 0.9.3 (b25cefe), macOS 26.5.2, Zig 0.16.0, from a clean native init.

Repro

// src/core.ts
import { Cmd, asciiBytes } from "@native-sdk/core";
import { type TextEditState, type TextInputEvent, applyTextInputEvent } from "@native-sdk/core/text";

export interface Model {
  readonly name: TextEditState;   // storing it in the Model is what triggers this
}

export type Msg =
  | { readonly kind: "name_edit"; readonly edit: TextInputEvent }
  | { readonly kind: "noop" };

export function initialModel(): Model {
  return { name: { text: asciiBytes(""), selection: { anchor: 0, focus: 0 }, composition: null } };
}

export function nameText(model: Model): Uint8Array {
  return model.name.text;
}

export function update(model: Model, msg: Msg): Model | [Model, Cmd<Msg>] {
  switch (msg.kind) {
    case "name_edit": {
      const next = applyTextInputEvent(model.name, msg.edit, 120);
      if (!next) return model;
      return { ...model, name: next };
    }
    case "noop":
      return model;
  }
}
<!-- src/app.native -->
<column gap="12" padding="16">
  <text-field text="{nameText}" placeholder="Type here" on-input="name_edit" />
</column>

Actual

sdk/text.ts:79  SC4023  'TextRange.start' / 'TextRange.end' - range failed:
                        proven range [-Infinity, 9007199254740991]
sdk/text.ts:243,244,325,355,375,394,414,439,459
                SC4022  'TextRange.start' / 'TextRange.end' - wholeness failed:
                        the value may be NaN

Line 79 is rangeNormalized, where Math.min is float-classed:

const start = Math.min(r.start, textLen);
const end = Math.min(r.end, textLen);

The SC4022 sites are other TextRange constructions - snapTextOffset(...), previousTextCaretOffset(...), { start: result.insertedStart, end: result.insertedEnd }.

Scope

Two things I checked before filing:

  • It does not reproduce unless TextEditState is a Model field. Calling applyTextInputEvent and storing only next.text builds fine - the integer slots are only instantiated once the record becomes model state.
  • Fixing rangeNormalized alone is not sufficient. Replacing that Math.min pair with | 0-pinned ternaries clears all four SC4023 errors at line 79, but the nine SC4022 sites remain. This looks systemic across TextRange construction rather than a single bad line.

Workaround

| 0 restores integer classification (the remedy in #251), applied both where a derived index is produced and at the slot it lands in. I hand-rolled an equivalent caret/selection editor that compiles under those rules, so the constraint is satisfiable in app code - it just isn't satisfied in the shipped module.

Impact

Without this, a TypeScript core cannot use the SDK's own text engine, so text fields are limited to whatever the app hand-rolls. In my case that meant append/backspace-only editing until I worked out the | 0 pattern - a user could not delete a word from the start of a field.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions