Something went wrong. Try again.
A native-speed rewrite of the roblox-ts compiler in golang
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392package incremental
import ( "context" "fmt" "slices"
"rotor/tsgo/ast" "rotor/tsgo/collections" "rotor/tsgo/compiler" "rotor/tsgo/core" "rotor/tsgo/diagnostics" "rotor/tsgo/json" "rotor/tsgo/outputpaths" "rotor/tsgo/tracing" "rotor/tsgo/tspath")
type SignatureUpdateKind byte
const ( SignatureUpdateKindComputedDts SignatureUpdateKind = iota SignatureUpdateKindStoredAtEmit SignatureUpdateKindUsedVersion)
type Program struct { snapshot *snapshot program *compiler.Program host Host
// Testing data testingData *TestingData}
var _ compiler.ProgramLike = (*Program)(nil)
func NewProgram(program *compiler.Program, oldProgram *Program, host Host, testing bool) *Program { incrementalProgram := &Program{ snapshot: programToSnapshot(program, oldProgram, testing), program: program, host: host, }
if testing { incrementalProgram.testingData = &TestingData{} incrementalProgram.testingData.SemanticDiagnosticsPerFile = &incrementalProgram.snapshot.semanticDiagnosticsPerFile if oldProgram != nil { incrementalProgram.testingData.OldProgramSemanticDiagnosticsPerFile = &oldProgram.snapshot.semanticDiagnosticsPerFile } else { incrementalProgram.testingData.OldProgramSemanticDiagnosticsPerFile = &collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName]{} } incrementalProgram.testingData.UpdatedSignatureKinds = make(map[tspath.Path]SignatureUpdateKind) } return incrementalProgram}
type TestingData struct { SemanticDiagnosticsPerFile *collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName] OldProgramSemanticDiagnosticsPerFile *collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName] UpdatedSignatureKinds map[tspath.Path]SignatureUpdateKind}
func (p *Program) GetTestingData() *TestingData { return p.testingData}
func (p *Program) panicIfNoProgram(method string) { if p.program == nil { panic(method + ": should not be called without program") }}
func (p *Program) GetProgram() *compiler.Program { p.panicIfNoProgram("GetProgram") return p.program}
func (p *Program) HasChangedDtsFile() bool { return p.snapshot.hasChangedDtsFile}
// Options implements compiler.AnyProgram interface.func (p *Program) Options() *core.CompilerOptions { return p.snapshot.options}
// CommonSourceDirectory implements compiler.AnyProgram interface.func (p *Program) CommonSourceDirectory() string { p.panicIfNoProgram("CommonSourceDirectory") return p.program.CommonSourceDirectory()}
// Program implements compiler.AnyProgram interface.func (p *Program) Program() *compiler.Program { p.panicIfNoProgram("Program") return p.program}
// IsSourceFileDefaultLibrary implements compiler.AnyProgram interface.func (p *Program) IsSourceFileDefaultLibrary(path tspath.Path) bool { p.panicIfNoProgram("IsSourceFileDefaultLibrary") return p.program.IsSourceFileDefaultLibrary(path)}
// GetSourceFiles implements compiler.AnyProgram interface.func (p *Program) GetSourceFiles() []*ast.SourceFile { p.panicIfNoProgram("GetSourceFiles") return p.program.GetSourceFiles()}
// GetSourceFile implements compiler.AnyProgram interface.func (p *Program) GetSourceFile(path string) *ast.SourceFile { p.panicIfNoProgram("GetSourceFile") return p.program.GetSourceFile(path)}
// GetConfigFileParsingDiagnostics implements compiler.AnyProgram interface.func (p *Program) GetConfigFileParsingDiagnostics() []*ast.Diagnostic { p.panicIfNoProgram("GetConfigFileParsingDiagnostics") return p.program.GetConfigFileParsingDiagnostics()}
// GetSyntacticDiagnostics implements compiler.AnyProgram interface.func (p *Program) GetSyntacticDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic { p.panicIfNoProgram("GetSyntacticDiagnostics") return p.program.GetSyntacticDiagnostics(ctx, file)}
// GetBindDiagnostics implements compiler.AnyProgram interface.func (p *Program) GetBindDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic { p.panicIfNoProgram("GetBindDiagnostics") return p.program.GetBindDiagnostics(ctx, file)}
func (p *Program) GetProgramDiagnostics() []*ast.Diagnostic { p.panicIfNoProgram("GetProgramDiagnostics") return p.program.GetProgramDiagnostics()}
func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic { p.panicIfNoProgram("GetGlobalDiagnostics") return p.program.GetGlobalDiagnostics(ctx)}
// GetSemanticDiagnostics implements compiler.AnyProgram interface.func (p *Program) GetSemanticDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic { p.panicIfNoProgram("GetSemanticDiagnostics") if p.snapshot.options.NoCheck.IsTrue() { return nil }
// Ensure all the diagnsotics are cached p.collectSemanticDiagnosticsOfAffectedFiles(ctx, file) if ctx.Err() != nil { return nil }
// Return result from cache if file != nil { return p.getSemanticDiagnosticsOfFile(file) }
var diagnostics []*ast.Diagnostic for _, file := range p.program.GetSourceFiles() { diagnostics = append(diagnostics, p.getSemanticDiagnosticsOfFile(file)...) } return diagnostics}
func (p *Program) getSemanticDiagnosticsOfFile(file *ast.SourceFile) []*ast.Diagnostic { cachedDiagnostics, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.Path()) if !ok { panic("After handling all the affected files, there shouldnt be more changes") } return slices.Concat( compiler.FilterNoEmitSemanticDiagnostics(cachedDiagnostics.getDiagnostics(p.program, file), p.snapshot.options), p.program.GetIncludeProcessorDiagnostics(file), )}
// GetDeclarationDiagnostics implements compiler.AnyProgram interface.func (p *Program) GetDeclarationDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic { p.panicIfNoProgram("GetDeclarationDiagnostics") result := emitFiles(ctx, p, compiler.EmitOptions{ TargetSourceFile: file, }, true) if result != nil { return result.Diagnostics } return nil}
// GetSuggestionDiagnostics implements compiler.AnyProgram interface.func (p *Program) GetSuggestionDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic { p.panicIfNoProgram("GetSuggestionDiagnostics") return p.program.GetSuggestionDiagnostics(ctx, file) // TODO: incremental suggestion diagnostics (only relevant in editor incremental builder?)}
// GetModeForUsageLocation implements compiler.AnyProgram interface.func (p *Program) Emit(ctx context.Context, options compiler.EmitOptions) *compiler.EmitResult { p.panicIfNoProgram("Emit")
var result *compiler.EmitResult if p.snapshot.options.NoEmit.IsTrue() { result = &compiler.EmitResult{EmitSkipped: true} } else { result = compiler.HandleNoEmitOnError(ctx, p, options.TargetSourceFile) if ctx.Err() != nil { return nil } } if result != nil { if options.TargetSourceFile != nil { return result }
// Emit buildInfo and combine result buildInfoResult := p.emitBuildInfo(ctx, options) if buildInfoResult != nil { result.Diagnostics = append(result.Diagnostics, buildInfoResult.Diagnostics...) result.EmittedFiles = append(result.EmittedFiles, buildInfoResult.EmittedFiles...) } return result } return emitFiles(ctx, p, options, false)}
// Handle affected files and cache the semantic diagnostics for all of them or the file asked forfunc (p *Program) collectSemanticDiagnosticsOfAffectedFiles(ctx context.Context, file *ast.SourceFile) { if p.snapshot.canUseIncrementalState() { // Get all affected files collectAllAffectedFiles(ctx, p) if ctx.Err() != nil { return }
if p.snapshot.semanticDiagnosticsPerFile.Size() == len(p.program.GetSourceFiles()) { // If we have all the files, return } }
var affectedFiles []*ast.SourceFile if file != nil { _, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.Path()) if ok { return } affectedFiles = []*ast.SourceFile{file} } else { for _, file := range p.program.GetSourceFiles() { if _, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.Path()); !ok { affectedFiles = append(affectedFiles, file) } } }
// Get their diagnostics and cache them diagnosticsPerFile := p.program.GetSemanticDiagnosticsWithoutNoEmitFiltering(ctx, affectedFiles) // commit changes if no err if ctx.Err() != nil { return }
// Commit changes to snapshot for file, diagnostics := range diagnosticsPerFile { p.snapshot.semanticDiagnosticsPerFile.Store(file.Path(), &DiagnosticsOrBuildInfoDiagnosticsWithFileName{diagnostics: diagnostics}) } if p.snapshot.semanticDiagnosticsPerFile.Size() == len(p.program.GetSourceFiles()) && p.snapshot.checkPending && !p.snapshot.options.NoCheck.IsTrue() { p.snapshot.checkPending = false } p.snapshot.buildInfoEmitPending.Store(true)}
func (p *Program) emitBuildInfo(ctx context.Context, options compiler.EmitOptions) *compiler.EmitResult { if tr := p.program.Tracing(); tr != nil { defer tr.Push(tracing.PhaseEmit, "emitBuildInfo", nil, true)() } buildInfoFileName := outputpaths.GetBuildInfoFileName(p.snapshot.options, tspath.ComparePathsOptions{ CurrentDirectory: p.program.GetCurrentDirectory(), UseCaseSensitiveFileNames: p.program.UseCaseSensitiveFileNames(), }) if buildInfoFileName == "" || p.program.IsEmitBlocked(buildInfoFileName) { return nil } if p.snapshot.hasErrors == core.TSUnknown { p.ensureHasErrorsForState(ctx, p.program) if p.snapshot.hasErrors != p.snapshot.hasErrorsFromOldState || p.snapshot.hasSemanticErrors != p.snapshot.hasSemanticErrorsFromOldState { p.snapshot.buildInfoEmitPending.Store(true) } } if !p.snapshot.buildInfoEmitPending.Load() { return nil } if ctx.Err() != nil { return nil } buildInfo := snapshotToBuildInfo(p.snapshot, p.program, buildInfoFileName) text, err := json.Marshal(buildInfo) if err != nil { panic(fmt.Sprintf("Failed to marshal build info: %v", err)) } if options.WriteFile != nil { err = options.WriteFile(buildInfoFileName, string(text), &compiler.WriteFileData{ BuildInfo: buildInfo, }) } else { err = p.program.Host().FS().WriteFile(buildInfoFileName, string(text)) } if err != nil { return &compiler.EmitResult{ EmitSkipped: true, Diagnostics: []*ast.Diagnostic{ ast.NewCompilerDiagnostic(diagnostics.Could_not_write_file_0_Colon_1, buildInfoFileName, err.Error()), }, } } p.snapshot.buildInfoEmitPending.Store(false) return &compiler.EmitResult{ EmitSkipped: false, EmittedFiles: []string{buildInfoFileName}, }}
func (p *Program) ensureHasErrorsForState(ctx context.Context, program *compiler.Program) { var hasIncludeProcessingDiagnostics func() bool var hasEmitDiagnostics bool if p.snapshot.canUseIncrementalState() { if slices.ContainsFunc(program.GetSourceFiles(), func(file *ast.SourceFile) bool { if _, ok := p.snapshot.emitDiagnosticsPerFile.Load(file.Path()); ok { // emit diagnostics will be encoded in buildInfo; return true } if hasIncludeProcessingDiagnostics == nil && len(p.program.GetIncludeProcessorDiagnostics(file)) > 0 { hasIncludeProcessingDiagnostics = func() bool { return true } } return false }) { hasEmitDiagnostics = true } if hasIncludeProcessingDiagnostics == nil { hasIncludeProcessingDiagnostics = func() bool { return false } } } else { hasEmitDiagnostics = p.snapshot.hasEmitDiagnostics hasIncludeProcessingDiagnostics = func() bool { return slices.ContainsFunc(program.GetSourceFiles(), func(file *ast.SourceFile) bool { return len(p.program.GetIncludeProcessorDiagnostics(file)) > 0 }) } }
if hasEmitDiagnostics { // Record this for only non incremental build info p.snapshot.hasErrors = core.IfElse(p.snapshot.options.IsIncremental(), core.TSFalse, core.TSTrue) // Dont need to encode semantic errors state since the emit diagnostics are encoded p.snapshot.hasSemanticErrors = false return }
if hasIncludeProcessingDiagnostics() || len(program.GetConfigFileParsingDiagnostics()) > 0 || len(program.GetSyntacticDiagnostics(ctx, nil)) > 0 || len(program.GetProgramDiagnostics()) > 0 || len(program.GetGlobalDiagnostics(ctx)) > 0 { p.snapshot.hasErrors = core.TSTrue // Dont need to encode semantic errors state since the syntax and program diagnostics are encoded as present p.snapshot.hasSemanticErrors = false return }
p.snapshot.hasErrors = core.TSFalse // Check semantic and emit diagnostics first as we dont need to ask program about it if slices.ContainsFunc(program.GetSourceFiles(), func(file *ast.SourceFile) bool { semanticDiagnostics, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.Path()) if !ok { // Missing semantic diagnostics in cache will be encoded in incremental buildInfo return p.snapshot.options.IsIncremental() } if len(semanticDiagnostics.diagnostics) > 0 || len(semanticDiagnostics.buildInfoDiagnostics) > 0 { // cached semantic diagnostics will be encoded in buildInfo return true } return false }) { // Because semantic diagnostics are recorded in buildInfo, we dont need to encode hasErrors in incremental buildInfo // But encode as errors in non incremental buildInfo p.snapshot.hasSemanticErrors = !p.snapshot.options.IsIncremental() }}