diff --git a/tools/lib/ponylang/pony_compiler/pony_compiler/pass.pony b/tools/lib/ponylang/pony_compiler/pony_compiler/pass.pony index c1487d01e6..e73eca57c3 100644 --- a/tools/lib/ponylang/pony_compiler/pony_compiler/pass.pony +++ b/tools/lib/ponylang/pony_compiler/pony_compiler/pass.pony @@ -10,27 +10,131 @@ type PassId is ( PassASM | PassObj | PassAll ) -primitive PassParse fun apply(): I32 => 0 -primitive PassSyntax fun apply(): I32 => 1 -primitive PassSugar fun apply(): I32 => 2 -primitive PassScope fun apply(): I32 => 3 -primitive PassImport fun apply(): I32 => 4 -primitive PassNameResolution fun apply(): I32 => 5 -primitive PassFlatten fun apply(): I32 => 6 -primitive PassTraits fun apply(): I32 => 7 -primitive PassRefer fun apply(): I32 => 8 -primitive PassExpr fun apply(): I32 => 9 -primitive PassCompleteness fun apply(): I32 => 10 -primitive PassVerify fun apply(): I32 => 11 -primitive PassFinaliser fun apply(): I32 => 12 -primitive PassSerialiser fun apply(): I32 => 13 -primitive PassReach fun apply(): I32 => 14 -primitive PassPaint fun apply(): I32 => 15 -primitive PassLLVMIR fun apply(): I32 => 16 -primitive PassBitcode fun apply(): I32 => 17 -primitive PassASM fun apply(): I32 => 18 -primitive PassObj fun apply(): I32 => 19 -primitive PassAll fun apply(): I32 => 20 +primitive PassParse is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 0 + fun string(): String iso^ => recover String .> append("parse") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassSyntax is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 1 + fun string(): String iso^ => recover String .> append("syntax") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassSugar is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 2 + fun string(): String iso^ => recover String .> append("sugar") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassScope is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 3 + fun string(): String iso^ => recover String .> append("scope") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassImport is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 4 + fun string(): String iso^ => recover String .> append("import") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassNameResolution is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 5 + fun string(): String iso^ => recover String .> append("name resolution") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassFlatten is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 6 + fun string(): String iso^ => recover String .> append("flatten") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassTraits is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 7 + fun string(): String iso^ => recover String .> append("traits") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassRefer is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 8 + fun string(): String iso^ => recover String .> append("refer") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassExpr is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 9 + fun string(): String iso^ => recover String .> append("expr") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassCompleteness is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 10 + fun string(): String iso^ => recover String .> append("completeness") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassVerify is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 11 + fun string(): String iso^ => recover String .> append("verify") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassFinaliser is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 12 + fun string(): String iso^ => recover String .> append("finaliser") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassSerialiser is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 13 + fun string(): String iso^ => recover String .> append("serialiser") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassReach is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 14 + fun string(): String iso^ => recover String .> append("reach") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassPaint is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 15 + fun string(): String iso^ => recover String .> append("paint") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassLLVMIR is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 16 + fun string(): String iso^ => recover String .> append("LLVM IR") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassBitcode is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 17 + fun string(): String iso^ => recover String .> append("bitcode") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassASM is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 18 + fun string(): String iso^ => recover String .> append("asm") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassObj is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 19 + fun string(): String iso^ => recover String .> append("obj") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) + +primitive PassAll is (Equatable[PassId] & Comparable[PassId]) + fun apply(): I32 => 20 + fun string(): String iso^ => recover String .> append("all") end + fun eq(that: box->PassId): Bool => this.apply() == that.apply() + fun lt(that: box->PassId): Bool => this.apply().lt(that.apply()) primitive _StrList """STUB""" diff --git a/tools/pony-lsp/compiler_notify.pony b/tools/pony-lsp/compiler_notify.pony index 25cd010678..c938e2244c 100644 --- a/tools/pony-lsp/compiler_notify.pony +++ b/tools/pony-lsp/compiler_notify.pony @@ -1,7 +1,9 @@ -use "pony_compiler" -use "term" use "cli" use "files" +use "collections" +use "term" + +use "pony_compiler" use @get_compiler_exe_directory[Bool]( output_path: Pointer[U8] tag, @@ -11,9 +13,10 @@ use @ponyint_pool_free_size[None](size: USize, p: Pointer[U8] tag) actor PonyCompiler is LspCompiler """ - Actor wrapping the pony_compiler `Compiler` - primitive to serialize compilation requests - (libponyc is not fully thread-safe). + Actor wrapping the pony_compiler `CompileSession` + to serialize compilation requests. + + Compilation will notify the caller for each requested pass. Determines the ponyc standard library location automatically by finding the executable directory @@ -113,10 +116,23 @@ actor PonyCompiler is LspCompiler be compile( package: FilePath, paths: Array[String val] val, - notify: CompilerNotify tag) + notify: CompilerNotify tag, + notify_passes: Array[PassId] val = [PassFinaliser]) => """ Compile a package with the given paths. + + Notify `notify` for every pass provided in `notify_passes`. The default is + to only notify once for the result of the finaliser pass. + + If an error happens while compiling up to the next pass, the `notify` will + be notified about the errors and no more notifications will arrive. + + Compilation is done when either the `result` param of + `CompilerNotify.on_compile_result()` is an Array of errors or if the `pass` + is the last one in the `notify_passes`. + + If `notify_passes` is empty, `notify` will never be called. """ if not this._got_settings then // enqueue compilation and dont execute it yet @@ -136,17 +152,45 @@ actor PonyCompiler is LspCompiler tmp.append(paths) tmp end - let result = - Compiler.compile( - package, - package_paths - where - user_flags = this._defines, - release = false, - verbosity = VerbosityQuiet, - limit = PassFinaliser) - let run_id = _run_id_gen = _run_id_gen + 1 - notify.done_compiling(package, result, run_id) + let sorted_passes = Sort[Array[PassId], PassId](notify_passes.clone()) + try + let first_pass = sorted_passes.shift()? + let run_id = _run_id_gen = _run_id_gen + 1 + let session = + CompileSession.create( + package, + package_paths + where + user_flags = this._defines, + release = false, + verbosity = VerbosityQuiet, + limit = first_pass) + try + let program = session.program() as Program val + notify.on_compile_result(package, run_id, first_pass, program) + for pass in sorted_passes.values() do + if session.continue_to(pass) then + // compilation success up until pass + let result = + try + session.program() as Program val + else + session.errors() + end + notify.on_compile_result(package, run_id, pass, result) + else + // compile error + notify.on_compile_result(package, run_id, pass, session.errors()) + // don't continue upon errors + break + end + end + else + // compile error up until first pass + notify.on_compile_result(package, run_id, first_pass, session.errors()) + end + session.dispose() + end trait tag LspCompiler """ @@ -164,20 +208,28 @@ trait tag LspCompiler be compile( package: FilePath, paths: Array[String val] val, - notify: CompilerNotify tag) + notify: CompilerNotify tag, + notify_passes: Array[PassId] val) """ - Compile the given package. + Compile the given package and call the `notify` with the compilation result + after the provided passes in `notify_passes`. """ interface CompilerNotify """ - Notify which is called by the compiler when it is done. + Notify which is called by the compiler when it is done with a requested pass. """ - be done_compiling( + be on_compile_result( package: FilePath, - result: (Program val | Array[Error val] val), - run: USize) + compile_run: USize, + pass: PassId, + result: (Program val | Array[Error val] val) + ) """ - Called when compilation completes. + Called when a compilation result after the provided pass is available. + + Do not keep the Program or any part of it around without copying, + if further passes are executed. + The underlying AST might be modified and parts of it replaced and deleted. """ diff --git a/tools/pony-lsp/language_server.pony b/tools/pony-lsp/language_server.pony index 9f52e52ce3..9c06777698 100644 --- a/tools/pony-lsp/language_server.pony +++ b/tools/pony-lsp/language_server.pony @@ -315,8 +315,7 @@ actor LanguageServer is (Notifier & RequestSender) "[" + n.method + "] No workspace found for '" + n.json().string() + "'") end - | Methods.workspace().did_change_configuration() - => + | Methods.workspace().did_change_configuration() => try let settings = JsonPathParser.compile("$.settings")?.query_one(n.params) as JsonValue diff --git a/tools/pony-lsp/test/message_handler.pony b/tools/pony-lsp/test/message_handler.pony index 2b0b2cf6bb..494463e84e 100644 --- a/tools/pony-lsp/test/message_handler.pony +++ b/tools/pony-lsp/test/message_handler.pony @@ -1,5 +1,6 @@ use ".." use "pony_test" +use "pony_compiler" use "files" actor TestCompiler is LspCompiler @@ -40,7 +41,8 @@ actor TestCompiler is LspCompiler be compile( package: FilePath, paths: Array[String val] val, - notify: CompilerNotify tag) + notify: CompilerNotify tag, + notify_passes: Array[PassId] val) => """ Most efficient compiler ever. diff --git a/tools/pony-lsp/workspace/state.pony b/tools/pony-lsp/workspace/state.pony index bb531b7ad0..9865adaf8e 100644 --- a/tools/pony-lsp/workspace/state.pony +++ b/tools/pony-lsp/workspace/state.pony @@ -98,12 +98,6 @@ class PackageState end end - fun ref add_diagnostic(run_id: USize, diagnostic: Diagnostic) => - if run_id >= this._compiler_run_id then - this._compiler_run_id = run_id - // TODO: - end - fun dispose() => for doc_state in this.documents.values() do doc_state.dispose() @@ -133,6 +127,10 @@ class DocumentState compiler_run_id = 0 fun ref update(run_id: USize, module': Module val) => + """ + Update the state of this document from the successful compilation from run + `run_id` with the given `module'`. + """ if run_id >= this.compiler_run_id then this.compiler_run_id = run_id // clear out diagnostics @@ -144,6 +142,8 @@ class DocumentState FromCompilerRun[Array[DocumentSymbol]] .create(run_id, DocumentSymbols.from_module(module', this._channel)) this._hash.update(run_id, module'.hash()) + // compilation was successful, clear out document diagnostics + this._diagnostics.update(run_id, []) end fun ref add_diagnostic(run_id: USize, diagnostic: Diagnostic) => diff --git a/tools/pony-lsp/workspace/workspace_manager.pony b/tools/pony-lsp/workspace/workspace_manager.pony index 040dfb9473..62343ff1bc 100644 --- a/tools/pony-lsp/workspace/workspace_manager.pony +++ b/tools/pony-lsp/workspace/workspace_manager.pony @@ -24,12 +24,29 @@ actor WorkspaceManager let _client: Client let _channel: Channel let _request_sender: RequestSender - let _compiler: LspCompiler let _packages: Map[String, PackageState] let _global_errors: Array[Diagnostic val] + let _compiler: LspCompiler + // we get notified about all passes, mostly for progress reporting + let _compile_passes: Array[PassId] val = + [ + PassParse + PassSyntax + PassSugar + PassScope + PassImport + PassNameResolution + PassFlatten + PassTraits + PassRefer + PassExpr + PassCompleteness + PassVerify + PassFinaliser + ] var _compile_run: USize = 0 - var _current_request: (RequestMessage val | None) = None var _compiling: Bool = false + var _current_request: (RequestMessage val | None) = None var _awaiting_compilation_for: Map[String, (USize | None)] = _awaiting_compilation_for.create() """ @@ -98,66 +115,146 @@ actor WorkspaceManager end error - be done_compiling( + fun _progress_report_notification( + package: FilePath, + pass: PassId) + : Notification + => + let token = this._compilation_token(package) + (let percentage: I64, let message: String) = + match pass + | PassParse => + (I64(5), "Pass: " + PassSyntax.string()) + | PassSyntax => + (I64(10), "Pass: " + PassSugar.string()) + | PassSugar => + (I64(15), "Pass: " + PassScope.string()) + | PassScope => + (I64(20), "Pass: " + PassImport.string()) + | PassImport => + (I64(25), "Pass: " + PassNameResolution.string()) + | PassNameResolution => + (I64(30), "Pass: " + PassFlatten.string()) + | PassFlatten => + (I64(35), "Pass: " + PassTraits.string()) + | PassTraits => + (I64(40), "Pass: " + PassRefer.string()) + | PassRefer => + (I64(50), "Pass: type-checking") // more meaningful than expr + | PassExpr => + (I64(85), "Pass: " + PassCompleteness.string()) + | PassCompleteness => + (I64(90), "Pass: " + PassVerify.string()) + | PassVerify => + (I64(95), "Pass: " + PassFinaliser.string()) + | PassFinaliser => + (I64(100), "Done") + else + // shouldn't happen + (I64(0), "") + end + Notification( + Methods.progress(), + JsonObject + .update("token", token) + .update( + "value", + JsonObject + .update("kind", "report") + .update( + "title", + "Compiling " + Path.base(package.path) + + " (" + this._compile_run.string() + ")") + .update("message", message) + .update("percentage", percentage) + .update("cancellable", false) + ) + ) + + be on_compile_result( program_dir: FilePath, - result: (Program val | Array[Error val] val), - run: USize) + run: USize, + pass: PassId, + result: (Program val | Array[Error val] val)) => """ Handle compilation completion. """ - if this._client.supports_window_work_done_progress() - then - let message = - match \exhaustive\ result - | let p: Program val => "Success" - | let errors: Array[Error val] val => errors.size().string() + " Errors" + if run > this._compile_run then + // notify client about progress + let notification = this._progress_report_nofitication(program_dir, pass) + if this._client.supports_window_work_done_progress() then + this._channel.send(notification) + end + + // group errors by file + let errors_by_file = Map[String, pc.Vec[JsonValue]].create() + + let done_compiling = + match (result, pass) + | (let program: Program val, PassFinaliser) + | (let errors: Array[Error val] val, _) => true + else + false end - this._channel.send( - Notification( - Methods.progress(), - JsonObject - .update( - "token", - this._compilation_token(program_dir)) - .update( - "value", + + // preparations when compilation is done - on error or success + if done_compiling then + if this._client.supports_window_work_done_progress() then + let message = + match \exhaustive\ result + | let p: Program val => "Success" + | let errors: Array[Error val] val => + errors.size().string() + " Errors" + end + this._channel.send( + Notification( + Methods.progress(), JsonObject - .update("kind", "end") - .update("message", message) + .update( + "token", + this._compilation_token(program_dir)) + .update( + "value", + JsonObject + .update("kind", "end") + .update("message", message)) ) - ) - ) - end - this._compiling = false - // TODO: check if we are awaiting compilation - // for some files and compare their hash against - // the current document hashes - this._channel.log( - "done compilation run " + run.string() + - " of " + program_dir.path) + ) + end + // mark that we are done with compilation + this._compiling = false - // ignore older compile runs - if run > this._compile_run then - this._compile_run = run - // group errors by file - let errors_by_file = Map[String, pc.Vec[JsonValue]].create(4) - - if this._client.supports_publish_diagnostics() then - // pre-fill with empty list of errors for - // all files for which we have errors now - for pkg in this._packages.values() do - for doc in pkg.documents.keys() do - errors_by_file(doc) = pc.Vec[JsonValue] + this._channel.log( + "done compilation run " + run.string() + + " of " + program_dir.path) + + // ignore older compile runs + this._compile_run = run.max(this._compile_run) + + if this._client.supports_publish_diagnostics() then + // pre-fill with empty list of errors for + // all files for which we have errors now + for pkg in this._packages.values() do + for doc in pkg.documents.keys() do + errors_by_file(doc) = pc.Vec[JsonValue] + end end end - end - // clear out global errors - this._global_errors.clear() + // clear out global errors + this._global_errors.clear() + end - match \exhaustive\ result - | let program: Program val => + // handle compilation result, + // if compilation is fully done (last pass) or not + match (result, pass) + | (let program: Program val, PassParse) => + // parsing was successful + // TODO: use parse pass data already + // to pre-fill some package and module states + None + | (let program: Program val, PassFinaliser) => this._channel.log("Successfully compiled " + program_dir.path) // create/update package states for every // package being part of the program @@ -198,10 +295,10 @@ actor WorkspaceManager this._compile(program_dir) end end - | let errors: Array[Error val] val => + | (let errors: Array[Error val] val, let err_pass: PassId) => this._channel.log( "Compilation failed with " + - errors.size().string() + " errors") + errors.size().string() + " errors after pass " + err_pass.string()) let diag_iter = Iter[Error](errors.values()) @@ -243,49 +340,41 @@ actor WorkspaceManager end end - // notify client about errors, if any - // create (or clear) error diagnostics - // message for each open file - if this._client.supports_publish_diagnostics() then - for file in errors_by_file.keys() do - try - let file_errors: pc.Vec[JsonValue] = errors_by_file(file)? - let msg = + if done_compiling then + // notify client about errors, if any + // create (or clear) error diagnostics + // message for each open file + if this._client.supports_publish_diagnostics() then + for file in errors_by_file.keys() do + try + let file_errors: pc.Vec[JsonValue] = errors_by_file(file)? + let msg = + Notification.create( + Methods.text_document().publish_diagnostics(), + JsonObject + .update("uri", Uris.from_path(file)) + .update("diagnostics", JsonArray(consume file_errors))) + this._channel.send(msg) + end + end + + // publish global diagnostics if any + let num_global_errs = this._global_errors.size() + if (num_global_errs > 0) then + let json_global_diagnostics = + pc.Vec[JsonValue].concat( + Iter[Diagnostic]( + this._global_errors.values()) + .map[JsonValue]({(diagnostic) => diagnostic.to_json() })) + let global_notifications = Notification.create( Methods.text_document().publish_diagnostics(), JsonObject - .update("uri", Uris.from_path(file)) - .update("diagnostics", JsonArray(consume file_errors))) - this._channel.send(msg) + .update("uri", "global") + .update("diagnostics", JsonArray(json_global_diagnostics))) + this._channel.send(global_notifications) end end - - // publish global diagnostics if any - let num_global_errs = this._global_errors.size() - if (num_global_errs > 0) then - let json_global_diagnostics = - pc.Vec[JsonValue].concat( - Iter[Diagnostic]( - this._global_errors.values()) - .map[JsonValue]({(diagnostic) => diagnostic.to_json() })) - let global_notifications = - Notification.create( - Methods.text_document().publish_diagnostics(), - JsonObject - .update("uri", "global") - .update("diagnostics", JsonArray(json_global_diagnostics))) - this._channel.send(global_notifications) - end - elseif this._client.supports_workspace_diagnostic_refresh() then - // we only need to issue a refresh if - // the client doesn't support publish - // diagnostics - - // tell the client to refresh all - // diagnostics for us - this._request_sender.send_request( - Methods.workspace().diagnostic().refresh(), - None) end else this._channel.log( @@ -305,8 +394,7 @@ actor WorkspaceManager ", ".join(workspace.dependency_paths.values())) let token = this._compilation_token(package) - if this._client.supports_window_work_done_progress() - then + if this._client.supports_window_work_done_progress() then let chan = this._channel let progress_begin_notification = Notification( @@ -321,6 +409,8 @@ actor WorkspaceManager "title", "Compiling " + Path.base(package.path) + " (" + this._compile_run.string() + ")") + .update("message", "Pass: Parse") + .update("percentage", I64(5)) .update("cancellable", false) ) ) @@ -336,7 +426,13 @@ actor WorkspaceManager end ) end - _compiler.compile(package, workspace.dependency_paths, this) + // get results after parse pass and after finaliser pass + _compiler.compile( + package, + workspace.dependency_paths, + this, + this._compile_passes + ) _compiling = true fun _compilation_token(package: FilePath): String =>