Repository navigation
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.1Save 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:
TypeScript/tsc/internal/api/encoder/stringtable.go
Lines 26 to 52 in ad24728
| 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