Compare commits

..

1 Commits

Author SHA1 Message Date
Irmen de Jong
9daa116148 first setup of LSP languageserver 2025-07-15 20:24:45 +02:00
18 changed files with 390 additions and 59 deletions

1
.idea/modules.xml generated
View File

@@ -15,6 +15,7 @@
<module fileurl="file://$PROJECT_DIR$/docs/docs.iml" filepath="$PROJECT_DIR$/docs/docs.iml" /> <module fileurl="file://$PROJECT_DIR$/docs/docs.iml" filepath="$PROJECT_DIR$/docs/docs.iml" />
<module fileurl="file://$PROJECT_DIR$/examples/examples.iml" filepath="$PROJECT_DIR$/examples/examples.iml" /> <module fileurl="file://$PROJECT_DIR$/examples/examples.iml" filepath="$PROJECT_DIR$/examples/examples.iml" />
<module fileurl="file://$PROJECT_DIR$/intermediate/intermediate.iml" filepath="$PROJECT_DIR$/intermediate/intermediate.iml" /> <module fileurl="file://$PROJECT_DIR$/intermediate/intermediate.iml" filepath="$PROJECT_DIR$/intermediate/intermediate.iml" />
<module fileurl="file://$PROJECT_DIR$/languageServer/languageServer.iml" filepath="$PROJECT_DIR$/languageServer/languageServer.iml" />
<module fileurl="file://$PROJECT_DIR$/parser/parser.iml" filepath="$PROJECT_DIR$/parser/parser.iml" /> <module fileurl="file://$PROJECT_DIR$/parser/parser.iml" filepath="$PROJECT_DIR$/parser/parser.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/prog8.iml" filepath="$PROJECT_DIR$/.idea/modules/prog8.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/prog8.iml" filepath="$PROJECT_DIR$/.idea/modules/prog8.iml" />
<module fileurl="file://$PROJECT_DIR$/simpleAst/simpleAst.iml" filepath="$PROJECT_DIR$/simpleAst/simpleAst.iml" /> <module fileurl="file://$PROJECT_DIR$/simpleAst/simpleAst.iml" filepath="$PROJECT_DIR$/simpleAst/simpleAst.iml" />

View File

@@ -19,7 +19,7 @@ class StatementOptimizer(private val program: Program,
val functionName = functionCallStatement.target.nameInSource[0] val functionName = functionCallStatement.target.nameInSource[0]
if (functionName in functions.purefunctionNames) { if (functionName in functions.purefunctionNames) {
if("ignore_unused" !in parent.definingBlock.options()) if("ignore_unused" !in parent.definingBlock.options())
errors.warn("statement has no effect (function return value is discarded)", functionCallStatement.position) errors.info("statement has no effect (function return value is discarded)", functionCallStatement.position)
return listOf(IAstModification.Remove(functionCallStatement, parent as IStatementContainer)) return listOf(IAstModification.Remove(functionCallStatement, parent as IStatementContainer))
} }
} }

View File

@@ -376,7 +376,7 @@ hline_filled_right .byte 0, %10000000, %11000000, %11100000, %11110000, %1111
_ormask .byte 128, 64, 32, 16, 8, 4, 2, 1 _ormask .byte 128, 64, 32, 16, 8, 4, 2, 1
; note: this can be even faster if we also have a 320 word x-lookup table, but hey, that's a lot of memory. ; note: this can be even faster if we also have a 320 word x-lookup table, but hey, that's a lot of memory.
; see https://codebase64.net/doku.php?id=base:various_techniques_to_calculate_adresses_fast_common_screen_formats_for_pixel_graphics ; see http://codebase64.org/doku.php?id=base:various_techniques_to_calculate_adresses_fast_common_screen_formats_for_pixel_graphics
; the y lookup tables encodes this formula: BITMAP_ADDRESS + 320*(py>>3) + (py & 7) (y from 0..199) ; the y lookup tables encodes this formula: BITMAP_ADDRESS + 320*(py>>3) + (py & 7) (y from 0..199)
; We use the 64tass syntax for range expressions to calculate this table on assembly time. ; We use the 64tass syntax for range expressions to calculate this table on assembly time.

View File

@@ -3,7 +3,7 @@
; ;
; some more interesting routines can be found here: ; some more interesting routines can be found here:
; http://6502org.wikidot.com/software-math ; http://6502org.wikidot.com/software-math
; https://codebase64.net/doku.php?id=base:6502_6510_maths ; http://codebase64.org/doku.php?id=base:6502_6510_maths
; https://github.com/TobyLobster/multiply_test ; https://github.com/TobyLobster/multiply_test
; https://github.com/TobyLobster/sqrt_test ; https://github.com/TobyLobster/sqrt_test
@@ -353,7 +353,7 @@ _divisor .word ?
randword .proc randword .proc
; -- 16 bit pseudo random number generator into AY ; -- 16 bit pseudo random number generator into AY
; default seed = $00c2 $1137. NOTE: uses self-modifying code so won't work in ROM (use randword_rom instead) ; default seed = $00c2 $1137. NOTE: uses self-modifying code so won't work in ROM (use randword_rom instead)
; routine from https://codebase64.net/doku.php?id=6502_6510_maths:x_abc_random_number_generator_8_16_bit ; routine from https://codebase64.org/doku.php?id=base:x_abc_random_number_generator_8_16_bit
inc x1 inc x1
clc clc
x1=*+1 x1=*+1
@@ -377,7 +377,7 @@ b1=*+1
randword_rom .proc randword_rom .proc
; -- 16 bit pseudo random number generator into AY. Can run from ROM. ; -- 16 bit pseudo random number generator into AY. Can run from ROM.
; NOTE: you have to set the initial seed using randseed_rom! (a good default seed = $00c2 $1137) ; NOTE: you have to set the initial seed using randseed_rom! (a good default seed = $00c2 $1137)
; routine from https://codebase64.net/doku.php?id=6502_6510_maths:x_abc_random_number_generator_8_16_bit ; routine from https://codebase64.org/doku.php?id=base:x_abc_random_number_generator_8_16_bit
inc _x1 inc _x1
clc clc
lda _x1 lda _x1

View File

@@ -380,7 +380,7 @@ _quadrant_region_to_direction:
asmsub atan2(ubyte x1 @R0, ubyte y1 @R1, ubyte x2 @R2, ubyte y2 @R3) -> ubyte @A { asmsub atan2(ubyte x1 @R0, ubyte y1 @R1, ubyte x2 @R2, ubyte y2 @R3) -> ubyte @A {
;; Calculate the angle, in a 256-degree circle, between two points into A. ;; Calculate the angle, in a 256-degree circle, between two points into A.
;; The points (x1, y1) and (x2, y2) have to use *unsigned coordinates only* from the positive quadrant in the carthesian plane! ;; The points (x1, y1) and (x2, y2) have to use *unsigned coordinates only* from the positive quadrant in the carthesian plane!
;; http://codebase64.net/doku.php?id=base:8bit_atan2_8-bit_angle ;; https://www.codebase64.org/doku.php?id=base:8bit_atan2_8-bit_angle
;; This uses 2 large lookup tables so uses a lot of memory but is super fast. ;; This uses 2 large lookup tables so uses a lot of memory but is super fast.
%asm {{ %asm {{

View File

@@ -1470,17 +1470,6 @@ internal class AstChecker(private val program: Program,
} }
override fun visit(functionCallStatement: FunctionCallStatement) { override fun visit(functionCallStatement: FunctionCallStatement) {
if(functionCallStatement.target.nameInSource.size==1) {
val functionName = functionCallStatement.target.nameInSource[0]
if (functionName in program.builtinFunctions.purefunctionNames) {
if("ignore_unused" !in functionCallStatement.parent.definingBlock.options()) {
errors.warn("statement has no effect (function return value is discarded)", functionCallStatement.position)
return
}
}
}
// most function calls, even to builtin functions, are still regular FunctionCall nodes here. // most function calls, even to builtin functions, are still regular FunctionCall nodes here.
// they get converted to the more specialized node type in BeforeAsmTypecastCleaner // they get converted to the more specialized node type in BeforeAsmTypecastCleaner
val targetStatement = functionCallStatement.target.checkFunctionOrLabelExists(program, functionCallStatement, errors) val targetStatement = functionCallStatement.target.checkFunctionOrLabelExists(program, functionCallStatement, errors)

View File

@@ -133,7 +133,7 @@ private fun integrateDefers(subdefers: Map<PtSub, List<PtDefer>>, program: PtPro
is PtNumber, is PtNumber,
is PtRange, is PtRange,
is PtString -> true is PtString -> true
is PtIdentifier -> true // actually PtIdentifier IS "complex" this time (it's a variable that might change) but it's kinda annoying to give a warning message for this very common case // note that unlike most other times, PtIdentifier IS "complex" this time (it's a variable that might change)
else -> false else -> false
} }

View File

@@ -5,7 +5,6 @@ import io.kotest.engine.spec.tempdir
import io.kotest.inspectors.shouldForAll import io.kotest.inspectors.shouldForAll
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain
import prog8.ast.expressions.NumericLiteral import prog8.ast.expressions.NumericLiteral
import prog8.ast.statements.Assignment import prog8.ast.statements.Assignment
import prog8.ast.statements.FunctionCallStatement import prog8.ast.statements.FunctionCallStatement
@@ -14,7 +13,6 @@ import prog8.code.core.BuiltinFunctions
import prog8.code.core.RegisterOrPair import prog8.code.core.RegisterOrPair
import prog8.code.core.isNumeric import prog8.code.core.isNumeric
import prog8.code.target.Cx16Target import prog8.code.target.Cx16Target
import prog8tests.helpers.ErrorReporterForTests
import prog8tests.helpers.compileText import prog8tests.helpers.compileText
class TestBuiltinFunctions: FunSpec({ class TestBuiltinFunctions: FunSpec({
@@ -105,31 +103,5 @@ main {
compileText(Cx16Target(), true, src, outputDir, writeAssembly = true) shouldNotBe null compileText(Cx16Target(), true, src, outputDir, writeAssembly = true) shouldNotBe null
} }
test("warning for return value discarding of pure functions") {
val src="""
main {
sub start() {
word @shared ww = 2222
abs(ww)
sgn(ww)
sqrt(ww)
min(ww, 0)
max(ww, 0)
clamp(ww, 0, 319)
}
}"""
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
compileText(Cx16Target(), true, src, outputDir, errors=errors, writeAssembly = false) shouldNotBe null
errors.warnings.size shouldBe 6
errors.warnings[0] shouldContain "statement has no effect"
errors.warnings[1] shouldContain "statement has no effect"
errors.warnings[2] shouldContain "statement has no effect"
errors.warnings[3] shouldContain "statement has no effect"
errors.warnings[4] shouldContain "statement has no effect"
errors.warnings[5] shouldContain "statement has no effect"
}
}) })

View File

@@ -27,7 +27,7 @@ of these library modules automatically as required.
.. note:: .. note::
Several algorithms and math routines in Prog8's assembly library files are adapted from Several algorithms and math routines in Prog8's assembly library files are adapted from
code publicly available on https://www.codebase64.net/ code publicly available on https://www.codebase64.org/
.. _builtinfunctions: .. _builtinfunctions:

View File

@@ -1,16 +1,27 @@
%option no_sysinit
%zeropage basicsafe
%import textio
main { main {
sub start() { sub start() {
word @shared ww = 2222 uword uw = 9999
word sw = -2222
ubyte ub = 42
byte sb = -99
bool bb = true
abs(ww) cx16.r0 = uw
sgn(ww) cx16.r0s = sw
sqrt(ww) cx16.r0L = ub
min(ww, 0) cx16.r0H = ub
max(ww, 0) cx16.r0sL = sb
clamp(ww, 0, 319) cx16.r0sH = sb
cx16.r0bL = bb
cx16.r0bH = bb
uw = cx16.r0
sw = cx16.r0s
ub = cx16.r0L
ub = cx16.r0H
sb = cx16.r0sL
sb = cx16.r0sH
bb = cx16.r0bL
bb = cx16.r0bH
} }
} }

View File

@@ -0,0 +1,103 @@
plugins {
kotlin("jvm")
id("application")
}
val debugPort = 8000
val debugArgs = "-agentlib:jdwp=transport=dt_socket,server=y,address=8000,suspend=n,quiet=y"
val serverMainClassName = "prog8lsp.MainKt"
val applicationName = "prog8-language-server"
application {
mainClass.set(serverMainClassName)
description = "Code completions, diagnostics and more for Prog8"
// applicationDefaultJvmArgs = listOf("-DkotlinLanguageServer.version=$version")
applicationDistribution.into("bin") {
filePermissions {
user {
read=true
execute=true
write=true
}
other.execute = true
group.execute = true
}
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.eclipse.lsp4j:org.eclipse.lsp4j:0.24.0")
implementation("org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.24.0")
}
configurations.forEach { config ->
config.resolutionStrategy {
preferProjectModules()
}
}
sourceSets.main {
java.srcDir("src")
resources.srcDir("resources")
}
sourceSets.test {
java.srcDir("src")
resources.srcDir("resources")
}
tasks.startScripts {
applicationName = "prog8-language-server"
}
tasks.register<Exec>("fixFilePermissions") {
// When running on macOS or Linux the start script
// needs executable permissions to run.
onlyIf { !System.getProperty("os.name").lowercase().contains("windows") }
commandLine("chmod", "+x", "${tasks.installDist.get().destinationDir}/bin/prog8-language-server")
}
tasks.register<JavaExec>("debugRun") {
mainClass.set(serverMainClassName)
classpath(sourceSets.main.get().runtimeClasspath)
standardInput = System.`in`
jvmArgs(debugArgs)
doLast {
println("Using debug port $debugPort")
}
}
tasks.register<CreateStartScripts>("debugStartScripts") {
applicationName = "prog8-language-server"
mainClass.set(serverMainClassName)
outputDir = tasks.installDist.get().destinationDir.toPath().resolve("bin").toFile()
classpath = tasks.startScripts.get().classpath
defaultJvmOpts = listOf(debugArgs)
}
tasks.register<Sync>("installDebugDist") {
dependsOn("installDist")
finalizedBy("debugStartScripts")
}
tasks.withType<Test>() {
testLogging {
events("failed")
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
}
}
tasks.installDist {
finalizedBy("fixFilePermissions")
}
tasks.build {
finalizedBy("installDist")
}

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="eclipse.lsp4j" level="project" />
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
</component>
</module>

View File

@@ -0,0 +1,34 @@
package prog8lsp
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.function.Supplier
private var threadCount = 0
class AsyncExecutor {
private val workerThread = Executors.newSingleThreadExecutor { Thread(it, "async${threadCount++}") }
fun execute(task: () -> Unit) =
CompletableFuture.runAsync(Runnable(task), workerThread)
fun <R> compute(task: () -> R) =
CompletableFuture.supplyAsync(Supplier(task), workerThread)
fun <R> computeOr(defaultValue: R, task: () -> R?) =
CompletableFuture.supplyAsync(Supplier {
try {
task() ?: defaultValue
} catch (e: Exception) {
defaultValue
}
}, workerThread)
fun shutdown(awaitTermination: Boolean) {
workerThread.shutdown()
if (awaitTermination) {
workerThread.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS)
}
}
}

View File

@@ -0,0 +1,19 @@
package prog8lsp
import org.eclipse.lsp4j.launch.LSPLauncher
import java.util.concurrent.Executors
import java.util.logging.Level
import java.util.logging.Logger
fun main(args: Array<String>) {
Logger.getLogger("").level = Level.INFO
val inStream = System.`in`
val outStream = System.out
val server = Prog8LanguageServer()
val threads = Executors.newSingleThreadExecutor { Thread(it, "client") }
val launcher = LSPLauncher.createServerLauncher(server, inStream, outStream, threads) { it }
server.connect(launcher.remoteProxy)
launcher.startListening()
}

View File

@@ -0,0 +1,46 @@
package prog8lsp
import org.eclipse.lsp4j.InitializeParams
import org.eclipse.lsp4j.InitializeResult
import org.eclipse.lsp4j.services.*
import java.io.Closeable
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletableFuture.completedFuture
import java.util.logging.Logger
class Prog8LanguageServer: LanguageServer, LanguageClientAware, Closeable {
private lateinit var client: LanguageClient
private val textDocuments = Prog8TextDocumentService()
private val workspaces = Prog8WorkspaceService()
private val async = AsyncExecutor()
private val logger = Logger.getLogger(Prog8LanguageServer::class.simpleName)
override fun initialize(params: InitializeParams): CompletableFuture<InitializeResult> = async.compute {
logger.info("Initializing LanguageServer")
InitializeResult()
}
override fun shutdown(): CompletableFuture<Any> {
close()
return completedFuture(null)
}
override fun exit() { }
override fun getTextDocumentService(): TextDocumentService = textDocuments
override fun getWorkspaceService(): WorkspaceService = workspaces
override fun connect(client: LanguageClient) {
logger.info("connecting to language client")
this.client = client
workspaces.connect(client)
textDocuments.connect(client)
}
override fun close() {
logger.info("closing down")
async.shutdown(awaitTermination = true)
}
}

View File

@@ -0,0 +1,62 @@
package prog8lsp
import org.eclipse.lsp4j.*
import org.eclipse.lsp4j.jsonrpc.messages.Either
import org.eclipse.lsp4j.services.LanguageClient
import org.eclipse.lsp4j.services.TextDocumentService
import java.util.concurrent.CompletableFuture
import java.util.logging.Logger
import kotlin.system.measureTimeMillis
class Prog8TextDocumentService: TextDocumentService {
private var client: LanguageClient? = null
private val async = AsyncExecutor()
private val logger = Logger.getLogger(Prog8TextDocumentService::class.simpleName)
fun connect(client: LanguageClient) {
this.client = client
}
override fun didOpen(params: DidOpenTextDocumentParams) {
logger.info("didOpen: $params")
}
override fun didChange(params: DidChangeTextDocumentParams) {
logger.info("didChange: $params")
}
override fun didClose(params: DidCloseTextDocumentParams) {
logger.info("didClose: $params")
}
override fun didSave(params: DidSaveTextDocumentParams) {
logger.info("didSave: $params")
}
override fun documentSymbol(params: DocumentSymbolParams): CompletableFuture<MutableList<Either<SymbolInformation, DocumentSymbol>>> = async.compute {
logger.info("Find symbols in ${params.textDocument.uri}")
val result: MutableList<Either<SymbolInformation, DocumentSymbol>>
val time = measureTimeMillis {
result = mutableListOf()
val range = Range(Position(1,1), Position(1,5))
val selectionRange = Range(Position(1,2), Position(1,10))
val symbol = DocumentSymbol("test-symbolName", SymbolKind.Constant, range, selectionRange)
result.add(Either.forRight(symbol))
}
logger.info("Finished in $time ms")
result
}
override fun completion(position: CompletionParams): CompletableFuture<Either<MutableList<CompletionItem>, CompletionList>> = async.compute{
logger.info("Completion for ${position}")
val result: Either<MutableList<CompletionItem>, CompletionList>
val time = measureTimeMillis {
val list = CompletionList(false, listOf(CompletionItem("test-completionItem")))
result = Either.forRight(list)
}
logger.info("Finished in $time ms")
result
}
// TODO add all other methods that get called.... :P
}

View File

@@ -0,0 +1,80 @@
package prog8lsp
import org.eclipse.lsp4j.*
import org.eclipse.lsp4j.jsonrpc.messages.Either
import org.eclipse.lsp4j.services.LanguageClient
import org.eclipse.lsp4j.services.WorkspaceService
import java.util.concurrent.CompletableFuture
import java.util.logging.Logger
class Prog8WorkspaceService: WorkspaceService {
private var client: LanguageClient? = null
private val logger = Logger.getLogger(Prog8WorkspaceService::class.simpleName)
fun connect(client: LanguageClient) {
this.client = client
}
override fun executeCommand(params: ExecuteCommandParams): CompletableFuture<Any> {
logger.info("executeCommand $params")
return super.executeCommand(params)
}
override fun symbol(params: WorkspaceSymbolParams): CompletableFuture<Either<MutableList<out SymbolInformation>, MutableList<out WorkspaceSymbol>>> {
logger.info("symbol $params")
return super.symbol(params)
}
override fun resolveWorkspaceSymbol(workspaceSymbol: WorkspaceSymbol): CompletableFuture<WorkspaceSymbol> {
logger.info("resolveWorkspaceSymbol $workspaceSymbol")
return super.resolveWorkspaceSymbol(workspaceSymbol)
}
override fun didChangeConfiguration(params: DidChangeConfigurationParams) {
logger.info("didChangeConfiguration: $params")
}
override fun didChangeWatchedFiles(params: DidChangeWatchedFilesParams) {
logger.info("didChangeWatchedFiles: $params")
}
override fun didChangeWorkspaceFolders(params: DidChangeWorkspaceFoldersParams) {
logger.info("didChangeWorkspaceFolders $params")
super.didChangeWorkspaceFolders(params)
}
override fun willCreateFiles(params: CreateFilesParams): CompletableFuture<WorkspaceEdit> {
logger.info("willCreateFiles $params")
return super.willCreateFiles(params)
}
override fun didCreateFiles(params: CreateFilesParams) {
logger.info("didCreateFiles $params")
super.didCreateFiles(params)
}
override fun willRenameFiles(params: RenameFilesParams): CompletableFuture<WorkspaceEdit> {
logger.info("willRenameFiles $params")
return super.willRenameFiles(params)
}
override fun didRenameFiles(params: RenameFilesParams) {
logger.info("didRenameFiles $params")
super.didRenameFiles(params)
}
override fun willDeleteFiles(params: DeleteFilesParams): CompletableFuture<WorkspaceEdit> {
logger.info("willDeleteFiles $params")
return super.willDeleteFiles(params)
}
override fun didDeleteFiles(params: DeleteFilesParams) {
logger.info("didDeleteFiles $params")
super.didDeleteFiles(params)
}
override fun diagnostic(params: WorkspaceDiagnosticParams): CompletableFuture<WorkspaceDiagnosticReport> {
logger.info("diagnostic $params")
return super.diagnostic(params)
}
}

View File

@@ -10,5 +10,6 @@ include(
':codeGenCpu6502', ':codeGenCpu6502',
':codeGenExperimental', ':codeGenExperimental',
':compiler', ':compiler',
':beanshell' ':beanshell',
':languageServer'
) )