Skip to content

Native API createSourceFile panics for numeric literals whose normalized text is longer than the source #65451

Open

Description

🔎 Search Terms

stringtable, createSourceFile panic, slice bounds, numeric literal encoder

🕗 Version & Regression Information

Reproduces with typescript@7.1.0-beta and typescript@7.1.0-dev.20261111.1 on Linux x64, using Node.js 24.20.0.

Also reproduces in an unmodified local build of current main, commit ad24728023c52ef6a92a278046cdb69621048365 (7.1.0-dev), built with node node_modules/hereby/bin/hereby.js build.

The first affected version has not been identified. This concerns the native typescript/sync API. I reviewed the FAQ and found no entry covering this crash.

⏯ Playground Link

Not applicable: this reproduces through the native API's source-file serialization.

💻 Code

Install an exact affected version:

npm install typescript@7.1.0-dev.20261111.1

Save as repro.cjs and run node repro.cjs:

const { API } = require('typescript/sync');
const api = new API();
try {
  api.createSourceFile('a.ts', '.1');
} finally {
  api.close();
}

🙁 Actual behavior

The call throws an error containing the server panic:

panic: runtime error: slice bounds out of range [-1:]
.../internal/api/encoder.(*stringTable).add
    .../internal/api/encoder/stringtable.go:41
.../internal/api/encoder.recordExtendedData_NumericLiteral
    .../internal/api/encoder/encoder.go:936

Replacing .1 with 1e3 or 1e9 also panics. 1e2, 1, and 0 succeed. Each input was tested in a separate API instance/process.

🙂 Expected behavior

Return a source-file syntax tree for these valid numeric expressions without a panic.

Additional information about the issue

The numeric literal's normalized text can be longer than its original spelling:

Source Normalized text Result
.1 0.1 Panic, slice start -1
1e3 1000 Panic, slice start -1
1e9 1000000000 Panic, slice start -7
1e2 100 Success
1 1 Success
0 0 Success

recordExtendedData_NumericLiteral passes the normalized text and original positions to stringTable.add. The latter computes start := end - len(text) and slices fileText[start:end] before checking whether the slice matches the text. In these examples start is negative, so the existing fallback for text that cannot be reused from the source is never reached.

Source inspected at commit ad24728023c52ef6a92a278046cdb69621048365:

index := uint32(len(t.offsets))
if kind == ast.KindSourceFile {
t.offsets = append(t.offsets, uint32(pos), uint32(end))
return index
}
length := len(text)
if end-pos > 0 && end <= len(t.fileText) {
// pos includes leading trivia, but we can usually infer the actual start of the
// string from the kind and end
endOffset := 0
if kind == ast.KindStringLiteral || kind == ast.KindTemplateTail || kind == ast.KindNoSubstitutionTemplateLiteral {
endOffset = 1
}
end = end - endOffset
start := end - length
fileSlice := t.fileText[start:end]
if fileSlice == text {
t.offsets = append(t.offsets, uint32(start), uint32(end))
return index
}
}
// no exact match, so we need to add it to the string table
offset := len(t.fileText) + t.otherStrings.Len()
t.otherStrings.WriteString(text)
t.offsets = append(t.offsets, uint32(offset), uint32(offset+length))
return index
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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