Skip to content

Type bare unsafe Err as string, not Infer - #108

Merged
samifouad merged 1 commit into
mainfrom
fix/bare-unsafe-err-types-string-103
Sep 10, 2026
Merged

Type bare unsafe Err as string, not Infer#108
samifouad merged 1 commit into
mainfrom
fix/bare-unsafe-err-types-string-103

Conversation

@samifouad

Copy link
Copy Markdown
Member

Closes dsc#103. Tracked under dsc#90 — no unchecked value without an explicit boundary contract.

What

Bare unsafe { } types as Result<Infer, string> instead of Result<Infer, Infer>.

The Err side was Infer, which accepts any member access. So this compiled and evaluated .message on a string:

const result = unsafe { deka.ui.renderToString(tree) }
match (result) {
  Err(e) => echo(e.message),   // undefined
}

dsc#102 fixed dsc#60 at the payload — the emitter normalizes a bare-unsafe Err to the thrown value's string representation, so errors-as-values holds at runtime. This makes the checker agree with the runtime, which is what turns the mistake into a check-time error with a span rather than a silent undefined.

Call sites

Every Err(e) => e.message migrated to Err(e) => e across the testsuite, the tour, and the fixtures.

Nothing caught these before. They are all .pass fixtures whose Err arm never executes, so the suite stayed green while every one of them would have printed undefined instead of the diagnostic it exists to surface.

Unchanged

The annotated form. unsafe<T> { } keeps Result<T, JsError> and its Error-object payload — sound, because the JsError member table (.message, .name) describes a real Error.

Scope

One step of the deka#252/#460 mandatory-annotation migration, not the whole thing.

Note on authorship

This is kimi's work. Its dispatch ended before it could commit — the second time that has happened — so I reviewed the diff and committed on its behalf. The checker change and all 30 call-site migrations were complete and consistent.

-kimi

-(typeck): bare `unsafe { }` now types as Result<Infer, string>. The Err side
  was Infer, which accepts any member access, so `Err(e) => e.message`
  compiled and evaluated .message on a string at runtime, yielding undefined.

  dsc#102 fixed dsc#60 at the payload: the emitter normalizes a bare unsafe Err
  to the thrown value string representation, so errors-as-values holds at
  runtime. This makes the checker agree with the runtime. That is what turns
  the mistake into a check-time error with a span instead of a silent
  undefined.

-(call sites): migrate every `Err(e) => e.message` to `Err(e) => e` across
  the testsuite, the tour, and the fixtures. Nothing caught these before: they
  are all .pass fixtures whose Err arm never executes, so the suite was green
  while every one of them would have printed undefined instead of the
  diagnostic it exists to surface.

-(annotated form): unchanged. `unsafe<T> { }` keeps Result<T, JsError> and its
  Error-object payload, which is sound because the member table (.message,
  .name) describes a real Error.

This is one step of the deka#252/#460 mandatory-annotation migration, not the
whole thing.
@samifouad
samifouad merged commit 025a787 into main Sep 10, 2026
4 checks passed
@samifouad
samifouad deleted the fix/bare-unsafe-err-types-string-103 branch September 10, 2026 09:02
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.

1 participant