From a5b89a486ca0d4feec6def6553c8d36f0c3c0a1b Mon Sep 17 00:00:00 2001 From: roshan84ya Date: Fri, 7 Aug 2026 20:12:53 +0530 Subject: [PATCH] Fix #186: every arm of a views module gets both doors, not just the nameable ones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A views module is `from` (put a value in) plus `as` (read one back out, #122). The reader was emitted only for a structured KIND (record/callback/tuple/named) or an arm with an explicit name hint — while the CONSTRUCTOR name falls back to the arm's kind. Two different sources for the same decision, so they disagreed, and an arm could get a one-way door: `fromArray: array => t` with no `asArray`. That split tracked NAMING, not soundness. The tell is that it produced distinctions nobody would defend: · `string[]` -> NO reader · `string[][]` -> reader, purely because TS names the inner element `Array` so a hint existed · `number` -> NO reader · `boolean` -> reader, only because `opaqueUnion` hard-codes `name: 'Bool'` The fix makes the gate MIRROR `fromName` instead of re-deriving the arm's identity: every arm reaching the generic `from*` branch gets both doors. This DELETES a special case rather than adding one. The arms that legitimately need no reader (`tagSet`, literal, `none`) each ARE their own runtime value and already `continue` above the gate. CONSUMER COST THIS CLOSES. Highcharts `ColorType = ColorString | GradientColorObject | PatternObject` is RETURNED by `color(…).get()` (`HighchartsSharedTypes.res:2550`, `get: option => ColorType.t`) and almost always holds the plain color string. With no `asString`, the only reader that compiled was `asGradientColorObject` — reading a string as a record, silently, since these views are unchecked. `ColorType.t` appears 1085 times in blend's bindings. `asString` is exactly as safe as the `asGradientColorObject` beside it: both are zero-cost views whose arm the caller must establish first, which is this module's contract either way. ONE DELIBERATE EXCLUSION, and it is now principled where the old one was incidental: a TYPE-VARIABLE arm keeps `fromTypeVar` with no reader, because `as…: t => 'a` unifies with ANY type at the call site — a universal unsafe cast, not a view of one arm. base-ui's `RootFilteredItems` is the real instance. MEASURED, then reconciled exactly. Across the baselines, 53 of 1624 views modules had a reader-less arm (42 of them the `string | number | record` shape). Counting the individual missing readers predicted 96; the fix adds 95, and the 1 remainder is the deliberately excluded typeVar arm. The baseline diff is 95 insertions and ZERO deletions or modifications across 6 files — no existing declaration changes, so unlike #185 there is no rename churn. No metrics.json moved: this adds capability without reclassifying anything. FIXTURE HAS TEETH IN BOTH DIRECTIONS, verified by breaking it two ways: · revert to the old name-hint gate -> 2 cases fail (views-module-readers, record-props) · drop the typeVar exclusion -> 1 case fails (views-module-readers) The fixture carries the `string[]` vs `string[][]` pair specifically so that a future change re-coupling the reader to the name hint makes them diverge again and fails. It also pins the typeVar exclusion in a GOLDEN rather than only in the benchmark, since the benchmark is an opt-in gate that a wholesale "emit readers unconditionally" simplification would sail past. npm test passes, 113 goldens match and compile, benchmark 10/10 identical. Co-Authored-By: Claude Opus 5 (1M context) --- .../bindings/CommonTypes.res | 2 + .../bindings/HighchartsSharedTypes.res | 30 +++++++ .../bindings/HighchartsSharedTypes.res | 30 +++++++ .../bindings/DirectoryTypes.res | 1 + .../bindings/HighchartsSharedTypes.res | 30 +++++++ .../baselines/hono/bindings/TypesTypes.res | 2 + docs/TYPE_MAPPING.md | 22 ++++- src/emit.mjs | 27 +++++- .../expected/RecordPropsTypes.res | 1 + .../views-module-readers/expected/Chart.res | 15 ++++ .../expected/ViewsModuleReadersTypes.res | 82 +++++++++++++++++++ .../views-module-readers/expected/_REPORT.md | 38 +++++++++ .../cases/views-module-readers/index.d.ts | 69 ++++++++++++++++ 13 files changed, 343 insertions(+), 6 deletions(-) create mode 100644 test/golden/cases/views-module-readers/expected/Chart.res create mode 100644 test/golden/cases/views-module-readers/expected/ViewsModuleReadersTypes.res create mode 100644 test/golden/cases/views-module-readers/expected/_REPORT.md create mode 100644 test/golden/cases/views-module-readers/index.d.ts diff --git a/benchmark/baselines/_base-ui-components_react/bindings/CommonTypes.res b/benchmark/baselines/_base-ui-components_react/bindings/CommonTypes.res index af5da62..94b22c2 100644 --- a/benchmark/baselines/_base-ui-components_react/bindings/CommonTypes.res +++ b/benchmark/baselines/_base-ui-components_react/bindings/CommonTypes.res @@ -70,7 +70,9 @@ module ValidateTarget = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromArray: array => t = "%identity" + external asArray: t => (array) = "%identity" external fromPromise: promise => t = "%identity" external asPromise: t => (promise) = "%identity" external fromUnit: unit => t = "%identity" diff --git a/benchmark/baselines/_juspay_blend-design-system_0.0.36/bindings/HighchartsSharedTypes.res b/benchmark/baselines/_juspay_blend-design-system_0.0.36/bindings/HighchartsSharedTypes.res index 3f481fc..a69cbb6 100644 --- a/benchmark/baselines/_juspay_blend-design-system_0.0.36/bindings/HighchartsSharedTypes.res +++ b/benchmark/baselines/_juspay_blend-design-system_0.0.36/bindings/HighchartsSharedTypes.res @@ -858,6 +858,7 @@ type patternObject = { module ColorType = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromGradientColorObject: gradientColorObject => t = "%identity" external asGradientColorObject: t => (gradientColorObject) = "%identity" external fromPatternObject: patternObject => t = "%identity" @@ -2517,7 +2518,9 @@ type sonificationSpeechPitchOptions = { module ChartsSonificationSpeechMappingOptionsPitch = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationSpeechPitchOptions: sonificationSpeechPitchOptions => t = "%identity" @@ -2534,7 +2537,9 @@ type sonificationSpeechPlayDelayOptions = { module ChartsSonificationSpeechMappingOptionsPlayDelay = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationSpeechPlayDelayOptions: sonificationSpeechPlayDelayOptions => t = "%identity" @@ -2543,7 +2548,9 @@ module ChartsSonificationSpeechMappingOptionsPlayDelay = { module ChartsSonificationSpeechMappingOptionsRate = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentTimeOptions: sonificationSpeechPlayDelayOptions => t = "%identity" @@ -2552,7 +2559,9 @@ module ChartsSonificationSpeechMappingOptionsRate = { module ChartsSonificationSpeechMappingOptionsVolume = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentVolumeOptions: sonificationSpeechPlayDelayOptions => t = "%identity" @@ -3526,7 +3535,9 @@ type plotColumnrangeBorderRadiusOptions = { module ChartsSeriesColumnrangeOptionsBorderRadius = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromBorderRadiusOptionsObject: borderRadiusOptionsObject => t = "%identity" external asBorderRadiusOptionsObject: t => (borderRadiusOptionsObject) = "%identity" external fromPlotColumnrangeBorderRadiusOptions: plotColumnrangeBorderRadiusOptions => t = "%identity" @@ -5483,7 +5494,9 @@ type plotRenkoBorderRadiusOptions = { module ChartsSeriesRenkoOptionsBorderRadius = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromBorderRadiusOptionsObject: borderRadiusOptionsObject => t = "%identity" external asBorderRadiusOptionsObject: t => (borderRadiusOptionsObject) = "%identity" external fromPlotRenkoBorderRadiusOptions: plotRenkoBorderRadiusOptions => t = "%identity" @@ -7700,7 +7713,9 @@ type sonificationInstrumentFrequencyOptions = { module ChartsSonificationInstrumentMappingOptionsFrequency = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentFrequencyOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7709,7 +7724,9 @@ module ChartsSonificationInstrumentMappingOptionsFrequency = { module ChartsSonificationInstrumentHighpassOptionsFrequency = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentLowpassFrequencyOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7718,7 +7735,9 @@ module ChartsSonificationInstrumentHighpassOptionsFrequency = { module ChartsSonificationInstrumentHighpassOptionsResonance = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentLowpassResonanceOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7740,7 +7759,9 @@ type sonificationInstrumentPitchOptions = { module ChartsSonificationInstrumentMappingOptionsPitch = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromStringOrNumbers: array => t = "%identity" @@ -7751,7 +7772,9 @@ module ChartsSonificationInstrumentMappingOptionsPitch = { module ChartsSonificationInstrumentMappingOptionsPlayDelay = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentPlayDelayOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7760,7 +7783,9 @@ module ChartsSonificationInstrumentMappingOptionsPlayDelay = { module ChartsSonificationInstrumentMappingOptionsRate = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationTracksRateOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7769,7 +7794,9 @@ module ChartsSonificationInstrumentMappingOptionsRate = { module ChartsSonificationInstrumentTremoloOptionsDepth = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentTremoloDepthOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7778,7 +7805,9 @@ module ChartsSonificationInstrumentTremoloOptionsDepth = { module ChartsSonificationInstrumentTremoloOptionsSpeed = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentTremoloSpeedOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -30981,6 +31010,7 @@ type exportingMenuObject<'a, 'b, 'c> = { module ChartsSeriesMapDataGeometryOptionsCoordinates = { type t external fromArray: array => t = "%identity" + external asArray: t => (array) = "%identity" external fromLonLatArrays: array> => t = "%identity" external asLonLatArrays: t => (array>) = "%identity" } diff --git a/benchmark/baselines/_juspay_blend-design-system_0.0.37-beta.8/bindings/HighchartsSharedTypes.res b/benchmark/baselines/_juspay_blend-design-system_0.0.37-beta.8/bindings/HighchartsSharedTypes.res index 731bf18..4413db8 100644 --- a/benchmark/baselines/_juspay_blend-design-system_0.0.37-beta.8/bindings/HighchartsSharedTypes.res +++ b/benchmark/baselines/_juspay_blend-design-system_0.0.37-beta.8/bindings/HighchartsSharedTypes.res @@ -917,6 +917,7 @@ type patternObject = { module ColorType = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromGradientColorObject: gradientColorObject => t = "%identity" external asGradientColorObject: t => (gradientColorObject) = "%identity" external fromPatternObject: patternObject => t = "%identity" @@ -2576,7 +2577,9 @@ type sonificationSpeechPitchOptions = { module ChartsSonificationSpeechMappingOptionsPitch = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationSpeechPitchOptions: sonificationSpeechPitchOptions => t = "%identity" @@ -2593,7 +2596,9 @@ type sonificationSpeechPlayDelayOptions = { module ChartsSonificationSpeechMappingOptionsPlayDelay = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationSpeechPlayDelayOptions: sonificationSpeechPlayDelayOptions => t = "%identity" @@ -2602,7 +2607,9 @@ module ChartsSonificationSpeechMappingOptionsPlayDelay = { module ChartsSonificationSpeechMappingOptionsRate = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentTimeOptions: sonificationSpeechPlayDelayOptions => t = "%identity" @@ -2611,7 +2618,9 @@ module ChartsSonificationSpeechMappingOptionsRate = { module ChartsSonificationSpeechMappingOptionsVolume = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentVolumeOptions: sonificationSpeechPlayDelayOptions => t = "%identity" @@ -3585,7 +3594,9 @@ type plotColumnrangeBorderRadiusOptions = { module ChartsSeriesColumnrangeOptionsBorderRadius = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromBorderRadiusOptionsObject: borderRadiusOptionsObject => t = "%identity" external asBorderRadiusOptionsObject: t => (borderRadiusOptionsObject) = "%identity" external fromPlotColumnrangeBorderRadiusOptions: plotColumnrangeBorderRadiusOptions => t = "%identity" @@ -5542,7 +5553,9 @@ type plotRenkoBorderRadiusOptions = { module ChartsSeriesRenkoOptionsBorderRadius = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromBorderRadiusOptionsObject: borderRadiusOptionsObject => t = "%identity" external asBorderRadiusOptionsObject: t => (borderRadiusOptionsObject) = "%identity" external fromPlotRenkoBorderRadiusOptions: plotRenkoBorderRadiusOptions => t = "%identity" @@ -7759,7 +7772,9 @@ type sonificationInstrumentFrequencyOptions = { module ChartsSonificationInstrumentMappingOptionsFrequency = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentFrequencyOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7768,7 +7783,9 @@ module ChartsSonificationInstrumentMappingOptionsFrequency = { module ChartsSonificationInstrumentHighpassOptionsFrequency = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentLowpassFrequencyOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7777,7 +7794,9 @@ module ChartsSonificationInstrumentHighpassOptionsFrequency = { module ChartsSonificationInstrumentHighpassOptionsResonance = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentLowpassResonanceOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7799,7 +7818,9 @@ type sonificationInstrumentPitchOptions = { module ChartsSonificationInstrumentMappingOptionsPitch = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromStringOrNumbers: array => t = "%identity" @@ -7810,7 +7831,9 @@ module ChartsSonificationInstrumentMappingOptionsPitch = { module ChartsSonificationInstrumentMappingOptionsPlayDelay = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentPlayDelayOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7819,7 +7842,9 @@ module ChartsSonificationInstrumentMappingOptionsPlayDelay = { module ChartsSonificationInstrumentMappingOptionsRate = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationTracksRateOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7828,7 +7853,9 @@ module ChartsSonificationInstrumentMappingOptionsRate = { module ChartsSonificationInstrumentTremoloOptionsDepth = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentTremoloDepthOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7837,7 +7864,9 @@ module ChartsSonificationInstrumentTremoloOptionsDepth = { module ChartsSonificationInstrumentTremoloOptionsSpeed = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentTremoloSpeedOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -31040,6 +31069,7 @@ type exportingMenuObject<'a, 'b, 'c> = { module ChartsSeriesMapDataGeometryOptionsCoordinates = { type t external fromArray: array => t = "%identity" + external asArray: t => (array) = "%identity" external fromLonLatArrays: array> => t = "%identity" external asLonLatArrays: t => (array>) = "%identity" } diff --git a/benchmark/baselines/_juspay_blend-design-system_0.0.37/bindings/DirectoryTypes.res b/benchmark/baselines/_juspay_blend-design-system_0.0.37/bindings/DirectoryTypes.res index d088ffd..6d6344e 100644 --- a/benchmark/baselines/_juspay_blend-design-system_0.0.37/bindings/DirectoryTypes.res +++ b/benchmark/baselines/_juspay_blend-design-system_0.0.37/bindings/DirectoryTypes.res @@ -108,6 +108,7 @@ type directoryVirtualizationConfig = { module DirectoryExpandedItems = { type t external fromArray: array => t = "%identity" + external asArray: t => (array) = "%identity" external fromSet: Set.t => t = "%identity" external asSet: t => (Set.t) = "%identity" } diff --git a/benchmark/baselines/_juspay_blend-design-system_0.0.37/bindings/HighchartsSharedTypes.res b/benchmark/baselines/_juspay_blend-design-system_0.0.37/bindings/HighchartsSharedTypes.res index 8d7bc50..e53f596 100644 --- a/benchmark/baselines/_juspay_blend-design-system_0.0.37/bindings/HighchartsSharedTypes.res +++ b/benchmark/baselines/_juspay_blend-design-system_0.0.37/bindings/HighchartsSharedTypes.res @@ -917,6 +917,7 @@ type patternObject = { module ColorType = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromGradientColorObject: gradientColorObject => t = "%identity" external asGradientColorObject: t => (gradientColorObject) = "%identity" external fromPatternObject: patternObject => t = "%identity" @@ -2576,7 +2577,9 @@ type sonificationSpeechPitchOptions = { module ChartsSonificationSpeechMappingOptionsPitch = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationSpeechPitchOptions: sonificationSpeechPitchOptions => t = "%identity" @@ -2593,7 +2596,9 @@ type sonificationSpeechPlayDelayOptions = { module ChartsSonificationSpeechMappingOptionsPlayDelay = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationSpeechPlayDelayOptions: sonificationSpeechPlayDelayOptions => t = "%identity" @@ -2602,7 +2607,9 @@ module ChartsSonificationSpeechMappingOptionsPlayDelay = { module ChartsSonificationSpeechMappingOptionsRate = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentTimeOptions: sonificationSpeechPlayDelayOptions => t = "%identity" @@ -2611,7 +2618,9 @@ module ChartsSonificationSpeechMappingOptionsRate = { module ChartsSonificationSpeechMappingOptionsVolume = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentVolumeOptions: sonificationSpeechPlayDelayOptions => t = "%identity" @@ -3585,7 +3594,9 @@ type plotColumnrangeBorderRadiusOptions = { module ChartsSeriesColumnrangeOptionsBorderRadius = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromBorderRadiusOptionsObject: borderRadiusOptionsObject => t = "%identity" external asBorderRadiusOptionsObject: t => (borderRadiusOptionsObject) = "%identity" external fromPlotColumnrangeBorderRadiusOptions: plotColumnrangeBorderRadiusOptions => t = "%identity" @@ -5542,7 +5553,9 @@ type plotRenkoBorderRadiusOptions = { module ChartsSeriesRenkoOptionsBorderRadius = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromBorderRadiusOptionsObject: borderRadiusOptionsObject => t = "%identity" external asBorderRadiusOptionsObject: t => (borderRadiusOptionsObject) = "%identity" external fromPlotRenkoBorderRadiusOptions: plotRenkoBorderRadiusOptions => t = "%identity" @@ -7759,7 +7772,9 @@ type sonificationInstrumentFrequencyOptions = { module ChartsSonificationInstrumentMappingOptionsFrequency = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentFrequencyOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7768,7 +7783,9 @@ module ChartsSonificationInstrumentMappingOptionsFrequency = { module ChartsSonificationInstrumentHighpassOptionsFrequency = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentLowpassFrequencyOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7777,7 +7794,9 @@ module ChartsSonificationInstrumentHighpassOptionsFrequency = { module ChartsSonificationInstrumentHighpassOptionsResonance = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentLowpassResonanceOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7799,7 +7818,9 @@ type sonificationInstrumentPitchOptions = { module ChartsSonificationInstrumentMappingOptionsPitch = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromStringOrNumbers: array => t = "%identity" @@ -7810,7 +7831,9 @@ module ChartsSonificationInstrumentMappingOptionsPitch = { module ChartsSonificationInstrumentMappingOptionsPlayDelay = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentPlayDelayOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7819,7 +7842,9 @@ module ChartsSonificationInstrumentMappingOptionsPlayDelay = { module ChartsSonificationInstrumentMappingOptionsRate = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationTracksRateOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7828,7 +7853,9 @@ module ChartsSonificationInstrumentMappingOptionsRate = { module ChartsSonificationInstrumentTremoloOptionsDepth = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentTremoloDepthOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -7837,7 +7864,9 @@ module ChartsSonificationInstrumentTremoloOptionsDepth = { module ChartsSonificationInstrumentTremoloOptionsSpeed = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" external fromFunction: JsFn.t => t = "%identity" external asFunction: t => (JsFn.t) = "%identity" external fromSonificationInstrumentTremoloSpeedOptions: sonificationInstrumentFrequencyOptions => t = "%identity" @@ -31040,6 +31069,7 @@ type exportingMenuObject<'a, 'b, 'c> = { module ChartsSeriesMapDataGeometryOptionsCoordinates = { type t external fromArray: array => t = "%identity" + external asArray: t => (array) = "%identity" external fromLonLatArrays: array> => t = "%identity" external asLonLatArrays: t => (array>) = "%identity" } diff --git a/benchmark/baselines/hono/bindings/TypesTypes.res b/benchmark/baselines/hono/bindings/TypesTypes.res index edbd906..f07358f 100644 --- a/benchmark/baselines/hono/bindings/TypesTypes.res +++ b/benchmark/baselines/hono/bindings/TypesTypes.res @@ -132,6 +132,7 @@ module MatchTarget = { external fromTuple2: ((array>, array)) => t = "%identity" external asTuple2: t => ((array>, array)) = "%identity" external fromArray: array>> => t = "%identity" + external asArray: t => (array>>) = "%identity" } type router = { name: string, @@ -165,6 +166,7 @@ module TypesHandler = { module TypesInput = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromRequest: WebTypes.request => t = "%identity" external asRequest: t => (WebTypes.request) = "%identity" external fromURL: WebTypes.url => t = "%identity" diff --git a/docs/TYPE_MAPPING.md b/docs/TYPE_MAPPING.md index bee160e..62b2c49 100644 --- a/docs/TYPE_MAPPING.md +++ b/docs/TYPE_MAPPING.md @@ -965,16 +965,32 @@ When a union can't be an `@unboxed` variant — **multiple object shapes**, or * (abstract members that `typeof`/`Array.isArray` can't split into a recognized variant shape) — it becomes an opaque-type module: an abstract `t` plus zero-cost `%identity` `from*` constructors. -**Every structured arm also gets its inverse `as*` READER (#122).** A module that only had `from*` +**Every arm also gets its inverse `as*` READER (#122).** A module that only had `from*` writers was write-only — you could put a `seriesLineOptions` into `options.series` but never read one back (`chartsOptionsSeries_t` is abstract), blocking consumers that inspect/transform existing options -(the portal's `mapOutages` reads `.name`/`.data` off each series). So each record/callback/tuple/named -arm now emits a symmetric `external as: t => armType = "%identity"` alongside its `from`. It's the +(the portal's `mapOutages` reads `.name`/`.data` off each series). So each arm emits a symmetric +`external as: t => armType = "%identity"` alongside its `from`. It's the zero-cost reverse view (value passes through unchanged), an **allowed `as*` form**; the caller discriminates on the union's runtime tag for arm-SPECIFIC fields, while fields common to every arm are always safe. Literal/tag/`unit` arms get no reader (the value IS its own runtime tag). Symmetric with the overloaded-function module, which was already reader-based. +The reader gate **mirrors the constructor's naming fallback** rather than re-deriving the arm's +identity — one arm, both doors. It previously required a structured *kind* (record/callback/tuple/named) +or an explicit name hint, while the `from*` name falls back to the arm's kind, so an arm with no +derivable hint got a one-way door: `fromArray: array => t` with **no `asArray`**. That tracked +naming, not soundness, and the tell was `string[]` having no reader while `string[][]` had one (TS names +the inner element `Array`, so a hint existed) — as did `boolean`, only because the collapsed bool arm +hard-codes its name. `asString: t => string` is exactly as unchecked as the `asRecord` beside it; both +require the caller to establish the arm first, which is the module's contract either way. The +consumer-visible cost was Highcharts `ColorType = ColorString | GradientColorObject | PatternObject`, +**returned** by `color(…).get()` and almost always holding the plain string: the only reader that +compiled was `asGradientColorObject`, silently reading a string as a record. 53 of 1624 views modules +across the benchmark baselines had a reader-less arm; 42 were the `string | number | record` shape. +A **type-variable** arm is the one deliberate exclusion — `as: t => 'a` unifies with any type at the +call site, making it a universal unsafe cast rather than a view of one arm. +Fixture: [`views-module-readers`](../test/golden/cases/views-module-readers). (#186) + Three member forms beyond plain `from*` constructors (#39): | Union member | Module arm | diff --git a/src/emit.mjs b/src/emit.mjs index 8b7134b..20f2cd9 100644 --- a/src/emit.mjs +++ b/src/emit.mjs @@ -964,9 +964,30 @@ function renderOpaque(t, lines, cfg, tAlias) { // build one. Symmetric to `from` and equally leak-free at the binding layer (the value // passes through unchanged). The caller discriminates on the union's runtime tag for // arm-SPECIFIC fields; fields common to every arm (`name`, `data`) are always safe to read. - // An allowed `as*` opaque-module accessor (CLAUDE.md). Emitted only for STRUCTURED arms - // (record/callback/tuple/named) — literal/tag/unit arms above are their own runtime value. - if (m.type.kind === 'typeRef' || m.type.kind === 'callback' || m.type.kind === 'tuple' || m.type.kind === 'record' || m.name) { + // An allowed `as*` opaque-module accessor (CLAUDE.md). + // + // The gate MIRRORS `fromName` rather than re-deriving the arm's identity: every arm reaching + // this generic branch gets BOTH doors. The arms that legitimately need no reader — `tagSet`, + // literal, `none` — each ARE their own runtime value and already `continue` above. + // + // It used to require a structured KIND or an explicit `m.name`, while `fromName` (above) falls + // back to `m.type.kind`. So an arm with a constructor but no derivable name hint — a + // primitive-element array, a bare `number` — got a one-way door: `fromArray` with no `asArray`. + // That split tracked NAMING, not soundness, and it showed: `string[]` had no reader while + // `string[][]` did (TS names the inner element `Array`, so a hint existed), and `boolean` did + // only because `opaqueUnion` hard-codes `name: 'Bool'` on the collapsed arm. + // + // Highcharts' `ColorType` (`ColorString | GradientColorObject | PatternObject`) is the + // consumer-visible cost: `color(…).get()` RETURNS one, it almost always holds the plain string, + // and the only reader that compiled was `asGradientColorObject` — reading a string as a record, + // silently, since these views are unchecked. `asString` is exactly as safe as the + // `asGradientColorObject` beside it: both are zero-cost views whose arm the caller must + // establish first, which is this module's contract either way. + // + // A TYPE-VARIABLE arm stays excluded, and that exclusion is principled where the old one was + // incidental: `as…: t => 'a` unifies with ANY type at the call site, so it is a universal + // unsafe cast rather than a view of one specific arm. (#186) + if (m.type.kind !== 'typeVar') { lines.push(` external as${fn.slice(4)}: ${rt} => (${rendered}) = "%identity"`) } } diff --git a/test/golden/cases/record-props/expected/RecordPropsTypes.res b/test/golden/cases/record-props/expected/RecordPropsTypes.res index 8266174..1a2f7c9 100644 --- a/test/golden/cases/record-props/expected/RecordPropsTypes.res +++ b/test/golden/cases/record-props/expected/RecordPropsTypes.res @@ -10,6 +10,7 @@ type recordPropsWeirdConfigWcv1r = { module RecordPropsWeird = { type t external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" external fromRecordPropsWeirdConfig: recordPropsWeirdConfigV3w34y => t = "%identity" external asRecordPropsWeirdConfig: t => (recordPropsWeirdConfigV3w34y) = "%identity" external fromRecordPropsWeirdConfig2: recordPropsWeirdConfigWcv1r => t = "%identity" diff --git a/test/golden/cases/views-module-readers/expected/Chart.res b/test/golden/cases/views-module-readers/expected/Chart.res new file mode 100644 index 0000000..ce3dd13 --- /dev/null +++ b/test/golden/cases/views-module-readers/expected/Chart.res @@ -0,0 +1,15 @@ +type props = { + filteredItems?: array, + borderRadius?: ViewsModuleReadersTypes.BorderRadius.t, + primArr?: ViewsModuleReadersTypes.PrimArr.t, + nestArr?: ViewsModuleReadersTypes.NestArr.t, // ⓘ was `NestArr` — opaque; build with NestArr.fromNamedA / NestArr.fromNamedB / NestArr.fromArrays + numArm?: ViewsModuleReadersTypes.NumArm.t, + boolArm?: ViewsModuleReadersTypes.BoolArm.t, // ⓘ was `BoolArm` — opaque; build with BoolArm.fromBool / BoolArm.fromNamedA / BoolArm.fromNamedB + withLiterals?: ViewsModuleReadersTypes.WithLiterals.t, // ⓘ was `WithLiterals` — opaque; build with WithLiterals.fromNamedA / WithLiterals.fromNamedB / WithLiterals.auto / WithLiterals.none + withTagRun?: ViewsModuleReadersTypes.WithTagRun.t, // ⓘ was `WithTagRun` — opaque; build with WithTagRun.fromNamedA / WithTagRun.fromNamedB / WithTagRun.fromTag + onBorderRadius?: ViewsModuleReadersTypes.BorderRadius.t => unit, + onPrimArr?: ViewsModuleReadersTypes.PrimArr.t => unit, +} + +@module("demo") +external make: React.component = "Chart" diff --git a/test/golden/cases/views-module-readers/expected/ViewsModuleReadersTypes.res b/test/golden/cases/views-module-readers/expected/ViewsModuleReadersTypes.res new file mode 100644 index 0000000..69c0285 --- /dev/null +++ b/test/golden/cases/views-module-readers/expected/ViewsModuleReadersTypes.res @@ -0,0 +1,82 @@ +type group<'b> = { + items: array<'b>, + label: string, +} +type namedA = { + a: string, +} +type namedB = { + b: float, +} +module ViewsModuleReadersFilteredItems = { + type t + external fromTypeVar: 'a => t = "%identity" + external fromGroup: group<'b> => t = "%identity" + external asGroup: t => (group<'b>) = "%identity" +} +module BorderRadius = { + type t + external fromString: string => t = "%identity" + external asString: t => (string) = "%identity" + external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" + external fromNamedA: namedA => t = "%identity" + external asNamedA: t => (namedA) = "%identity" + external fromNamedB: namedB => t = "%identity" + external asNamedB: t => (namedB) = "%identity" +} +module PrimArr = { + type t + external fromNamedA: namedA => t = "%identity" + external asNamedA: t => (namedA) = "%identity" + external fromNamedB: namedB => t = "%identity" + external asNamedB: t => (namedB) = "%identity" + external fromArray: array => t = "%identity" + external asArray: t => (array) = "%identity" +} +module NestArr = { + type t + external fromNamedA: namedA => t = "%identity" + external asNamedA: t => (namedA) = "%identity" + external fromNamedB: namedB => t = "%identity" + external asNamedB: t => (namedB) = "%identity" + external fromArrays: array> => t = "%identity" + external asArrays: t => (array>) = "%identity" +} +module NumArm = { + type t + external fromNumber: float => t = "%identity" + external asNumber: t => (float) = "%identity" + external fromNamedA: namedA => t = "%identity" + external asNamedA: t => (namedA) = "%identity" + external fromNamedB: namedB => t = "%identity" + external asNamedB: t => (namedB) = "%identity" +} +module BoolArm = { + type t + external fromBool: bool => t = "%identity" + external asBool: t => (bool) = "%identity" + external fromNamedA: namedA => t = "%identity" + external asNamedA: t => (namedA) = "%identity" + external fromNamedB: namedB => t = "%identity" + external asNamedB: t => (namedB) = "%identity" +} +module WithLiterals = { + type t + external fromNamedA: namedA => t = "%identity" + external asNamedA: t => (namedA) = "%identity" + external fromNamedB: namedB => t = "%identity" + external asNamedB: t => (namedB) = "%identity" + external fromAuto: [#"auto"] => t = "%identity" + let auto: t = fromAuto(#"auto") + external fromNone: [#"none"] => t = "%identity" + let none: t = fromNone(#"none") +} +module WithTagRun = { + type t + external fromNamedA: namedA => t = "%identity" + external asNamedA: t => (namedA) = "%identity" + external fromNamedB: namedB => t = "%identity" + external asNamedB: t => (namedB) = "%identity" + external fromTag: [#"aa" | #"bb" | #"cc" | #"dd" | #"ee"] => t = "%identity" +} diff --git a/test/golden/cases/views-module-readers/expected/_REPORT.md b/test/golden/cases/views-module-readers/expected/_REPORT.md new file mode 100644 index 0000000..e3e480b --- /dev/null +++ b/test/golden/cases/views-module-readers/expected/_REPORT.md @@ -0,0 +1,38 @@ +# Binding report — `demo` + +**1** components · ✅ **1** usable · 🔍 **0** need review · 🛑 **0** broken + +**11** shared types deduplicated into **1** `*Types.res` modules (referenced qualified — no per-file redeclaration). + +## 📦 Dependencies + +| Kind | Package | Provides | Status | +|------|---------|----------|--------| +| required | `@rescript/react + stdlib` | JsxDOM, Dom, React, ReactEvent | ✓ present | +| optional | `rescript-webapi` | File, FileList | ✗ not installed | + +## ✅ Usable + +These compile and every prop is bound type-safely — use them directly. +_(n loose)_ = some props widened to `string`; they still work, just loosely typed. + +- Chart + +## ⚪ Loosely typed (widened to `string`) + +These resolved to a real but complex type and were widened to `string` (they compile and work). Grouped by type so you can review each pattern once — confirm `string` is acceptable, or it may deserve a tighter mapping. + +_(none)_ + +## 🔍 Needs review + +A multi-type prop couldn't be auto-discriminated at runtime (e.g. two object shapes), so an `@unboxed` variant won't work and we **refuse to use `%identity`/unsafe casts**. The prop is emitted as a `string` placeholder with an inline `// ⚠️ REVIEW` comment — bind it by hand or fix the type upstream. + +_(none)_ + +## 🛑 Broken — needs serious component change + +These props resolved to `unknown`/`any` (usually a generic `T`). They're emitted as a placeholder so the file still compiles, but **the props will not work as typed** — they need a concrete type upstream, or generic-binding support. + +_(none)_ 🎉 + diff --git a/test/golden/cases/views-module-readers/index.d.ts b/test/golden/cases/views-module-readers/index.d.ts new file mode 100644 index 0000000..433911c --- /dev/null +++ b/test/golden/cases/views-module-readers/index.d.ts @@ -0,0 +1,69 @@ +// #186 — EVERY ARM OF A VIEWS MODULE GETS BOTH DOORS. +// +// A views module is `from` (put a value in) + `as` (read one back out, #122). The reader used to +// be gated on a structured KIND or an explicit name hint, while the CONSTRUCTOR name falls back to +// `m.type.kind` — two different sources, so they disagreed and an arm could get a one-way door: +// +// external fromArray: array => t = "%identity" // no asArray +// +// That split tracked NAMING, not soundness. The give-away is `primArr` vs `nestArr` below: `string[]` +// had NO reader but `string[][]` DID, purely because TS names the inner element `Array` so a hint +// existed. `boolArm` likewise had one only because `opaqueUnion` hard-codes `name: 'Bool'`. Nothing +// about safety separates these — `asArray: t => array` is exactly as unchecked as the +// `asNamedA` beside it, and both require the caller to establish the arm first. +// +// REAL COST: Highcharts' `ColorType = ColorString | GradientColorObject | PatternObject` is returned by +// `color(…).get()` and almost always holds the plain string. `asString` didn't exist, so the only +// reader that compiled was `asGradientColorObject` — a silent misread of a string as a record. 53 of +// 1624 views modules in the benchmark baselines had at least one reader-less arm; 42 of those were this +// `string | number | record` shape. +type JsxElement = { __brand: 'element' } + +interface NamedA { a: string } +interface NamedB { b: number } + +// The Highcharts shape, 42 of the 53. Two named object arms carry the module; `string` and `number` +// are the arms that had constructors and no readers, and they are the COMMON runtime cases. +type BorderRadius = number | string | NamedA | NamedB + +// THE ASYMMETRY PAIR — these two must now match. Before the fix `primArr` emitted `fromArray` alone +// while `nestArr` emitted `fromArrays` + `asArrays`; if a future change re-couples the reader to the +// name hint, these two diverge again and this case fails. +type PrimArr = NamedA | NamedB | string[] +type NestArr = NamedA | NamedB | string[][] + +// Bare primitive arms. `bool` already had a reader (hard-coded hint); `number` did not. +type NumArm = NamedA | NamedB | number +type BoolArm = NamedA | NamedB | boolean + +// ARMS THAT CORRECTLY GET NO READER — each IS its own runtime value, and all three `continue` before +// the gate. `'auto'`/`'none'` are ready-made constants; a run of >= 4 literals folds into ONE `fromTag` +// polyvar. A reader for either would hand back a value the polyvar already pins exactly. +type WithLiterals = NamedA | NamedB | 'auto' | 'none' +type WithTagRun = NamedA | NamedB | 'aa' | 'bb' | 'cc' | 'dd' | 'ee' + +// THE ONE DELIBERATE EXCLUSION — a TYPE-VARIABLE arm gets `fromTypeVar` and NO reader, because +// `asTypeVar: t => 'e` unifies with ANY type at the call site: a universal unsafe cast, not a view of +// one arm. Every other reader here is pinned to a concrete arm and can only misread WITHIN this union. +// This is base-ui's real `AriaCombobox.filteredItems` shape (`readonly any[] | readonly Group[]`, +// which collapses to one array whose ELEMENT is the union) — prop-position `any` becomes a type var, +// so the module arms are the type var and `group<'f>`. Pinned by a golden and not only by the +// benchmark, since the benchmark is an opt-in gate that a wholesale "emit readers unconditionally" +// simplification would sail past. +type Group = { items: T[]; label: string } + +// RECEIVE POSITION — polarity lets a readable module through here (#39/#122) precisely because the +// consumer can inspect what the library produced. With no `asString`/`asNumber` that promise was +// unmet: the value arrives and the primitive cases are unreadable. +export declare const Chart: (props: { + filteredItems?: readonly any[] | readonly Group[] + borderRadius?: BorderRadius + primArr?: PrimArr + nestArr?: NestArr + numArm?: NumArm + boolArm?: BoolArm + withLiterals?: WithLiterals + withTagRun?: WithTagRun + onBorderRadius?: (v: BorderRadius) => void + onPrimArr?: (v: PrimArr) => void +}) => JsxElement