Skip to content
Merged
22 changes: 8 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Get started with SAMT, learn fundamental concepts or extend SAMT with a custom g

- [Getting Started](https://github.com/samtkit/core/wiki/Getting-Started)
- [Modeling Concepts](https://github.com/samtkit/core/wiki/Modeling-Concepts)
- [SAMT Template](https://github.com/samtkit/template)
- [Visual Studio Code Plugin](https://marketplace.visualstudio.com/items?itemName=samt.samt)

### Advanced
Expand All @@ -31,31 +32,24 @@ Get started with SAMT, learn fundamental concepts or extend SAMT with a custom g

## Development Setup

SAMT is written in [Kotlin](https://kotlinlang.org/) and uses [Gradle](https://gradle.org/) as a build tool, for the best developer experience we recommend using [IntelliJ](https://www.jetbrains.com/idea/).
SAMT is written in [Kotlin](https://kotlinlang.org/) and uses [Gradle](https://gradle.org/) as a build tool,
for the best developer experience we recommend using [IntelliJ IDEA](https://www.jetbrains.com/idea/).

If you want to start SAMT locally, simply clone the repository and compile it using Gradle:

```shell
./gradlew assemble
```

You can also compile the CLI module locally:
And then use this locally compiled SAMT to compile your SAMT files:

```shell
./gradlew :cli:shadowJar
java -jar ./cli/build/libs/samt-cli.jar compile ./specification/examples/todo-service/*.samt
```

And then compile SAMT files using this locally compiled version:

```shell
java -jar ./cli/build/libs/samt-cli.jar ./specification/examples/todo-service/*.samt
```

If you're more interested in the [SAMT Visual Studio Code plugin](https://github.com/samtkit/vscode) or the related language server, you can also compile it locally as well:

```shell
./gradlew :language-server:shadowJar
```
If you are interested in learning about the functionality and operation of the [SAMT Visual Studio Code Plugin](https://github.com/samtkit/vscode)
or methods for running and debugging the related language server on your local machine,
related documentation is available in the [SAMT VS Code Wiki](https://github.com/samtkit/vscode/wiki).

## Contributing

Expand Down
4 changes: 3 additions & 1 deletion buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ kotlin {
}

dependencies {
implementation("org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin:1.8.21")
val kotlin = "1.8.21"
implementation("org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin:$kotlin")
implementation("org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin:$kotlin")
}

repositories {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
kotlin("jvm")
kotlin("plugin.serialization") apply false
}

apply(plugin = "kover")
Expand Down
2 changes: 2 additions & 0 deletions cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ plugins {
application
id("samt-core.kotlin-conventions")
alias(libs.plugins.shadow)
kotlin("plugin.serialization")
}

dependencies {
implementation(libs.jCommander)
implementation(libs.mordant)
implementation(libs.kotlinx.serialization.json)
implementation(project(":common"))
implementation(project(":lexer"))
implementation(project(":parser"))
Expand Down
4 changes: 1 addition & 3 deletions cli/src/main/kotlin/tools/samt/cli/ASTPrinter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.github.ajalt.mordant.rendering.TextStyles.bold
import com.github.ajalt.mordant.rendering.TextStyles.underline
import tools.samt.parser.*

object ASTPrinter {
internal object ASTPrinter {
fun dump(node: Node): String = buildString {
val topLevelStyle = (red + bold + underline)
val expressionStyle = (blue + bold)
Expand Down Expand Up @@ -53,8 +53,6 @@ object ASTPrinter {
firstLine = false
}
}

appendLine()
}

private fun dumpInfo(node: Node): String? = when (node) {
Expand Down
95 changes: 25 additions & 70 deletions cli/src/main/kotlin/tools/samt/cli/App.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package tools.samt.cli

import com.beust.jcommander.JCommander

import com.github.ajalt.mordant.terminal.Terminal
import tools.samt.common.DiagnosticController
import tools.samt.common.SourceFile
import tools.samt.semantic.SemanticModelBuilder
import java.io.File

fun main(args: Array<String>) {
val cliArgs = CliArgs()
val compileCommand = CompileCommand()
val dumpCommand = DumpCommand()
val wrapperCommand = WrapperCommand()
val jCommander = JCommander.newBuilder()
.addObject(cliArgs)
.addCommand("compile", compileCommand)
.addCommand("dump", dumpCommand)
.addCommand("wrapper", wrapperCommand)
.programName("./samtw")
.build()
jCommander.parse(*args)
Expand All @@ -20,77 +22,30 @@ fun main(args: Array<String>) {
return
}

val t = Terminal()
val terminal = Terminal()

val workingDirectory = System.getProperty("user.dir")
val filePaths = cliArgs.files

val startTimestamp = System.currentTimeMillis()
val diagnosticController = DiagnosticController(workingDirectory).also { controller ->

// must specify at least one SAMT file
if (filePaths.isEmpty()) {
controller.reportGlobalError("No files specified")
return@also
}

// attempt to load each source file
val sourceFiles = buildList {
for (path in filePaths) {
val file = File(path)

if (!file.exists()) {
controller.reportGlobalError("File '$path' does not exist")
continue
}

if (!file.canRead()) {
controller.reportGlobalError("File '$path' cannot be read, bad file permissions?")
continue
}

if (file.extension != "samt") {
controller.reportGlobalError("File '$path' must end in .samt")
continue
}

val source = file.readText()
add(SourceFile(file.canonicalPath, source))
}
}

// if any source files failed to load, exit
if (controller.hasErrors()) {
return@also
}

// attempt to parse each source file into an AST
val fileNodes = buildList {
for (source in sourceFiles) {
val context = controller.createContext(source)
val fileNode = parseSourceFile(source, context)
if (fileNode != null) {
add(fileNode)
}
}
}

// if any source files failed to parse, exit
if (controller.hasErrors()) {
return@also
}
val controller = DiagnosticController(workingDirectory)

// if the user requested the AST to be dumped, do so
if (cliArgs.dumpAst) {
fileNodes.forEach {
t.print(ASTPrinter.dump(it))
}
val startTimestamp = System.currentTimeMillis()
when (jCommander.parsedCommand) {
"compile" -> compile(compileCommand, controller)
"dump" -> dump(dumpCommand, terminal, controller)
"wrapper" -> wrapper(wrapperCommand, terminal, controller)
else -> {
jCommander.usage()
return
}

// build up the semantic model from the AST
SemanticModelBuilder.build(fileNodes, controller)
}

val currentTimestamp = System.currentTimeMillis()
t.println(DiagnosticFormatter.format(diagnosticController, startTimestamp, currentTimestamp, terminalWidth = t.info.width))

terminal.println(
DiagnosticFormatter.format(
controller,
startTimestamp,
currentTimestamp,
terminalWidth = terminal.info.width
)
)
}
35 changes: 33 additions & 2 deletions cli/src/main/kotlin/tools/samt/cli/CliArgs.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
package tools.samt.cli

import com.beust.jcommander.Parameter
import com.beust.jcommander.Parameters

class CliArgs {
@Parameter(names = ["-h", "--help"], description = "Display help", help = true)
var help: Boolean = false
}

@Parameters(commandDescription = "Compile SAMT files")
class CompileCommand {
@Parameter(description = "Files to compile, defaults to all .samt files in the current directory")
var files: List<String> = mutableListOf()
}

@Parameters(commandDescription = "Dump SAMT files in various formats for debugging purposes")
class DumpCommand {
@Parameter(names = ["--tokens"], description = "Dump a visual representation of the token stream")
var dumpTokens: Boolean = false

@Parameter(names = ["--dump-ast"], description = "Dump a visual representation of the AST", help = true)
@Parameter(names = ["--ast"], description = "Dump a visual representation of the AST")
var dumpAst: Boolean = false

@Parameter(description = "Files")
@Parameter(names = ["--types"], description = "Dump a visual representation of the resolved types")
var dumpTypes: Boolean = false

@Parameter(description = "Files to dump, defaults to all .samt files in the current directory")
var files: List<String> = mutableListOf()
}

@Parameters(commandDescription = "Initialize or update the SAMT wrapper")
class WrapperCommand {
@Parameter(names = ["--version"], description = "The SAMT version to use, defaults to the latest version published on GitHub")
var version: String = "latest"

@Parameter(names = ["--version-source"], description = "The location from where the latest version will be fetched from, defaults to the GitHub API. The result must be a JSON object with a 'tag_name' field containing the version string")
var latestVersionSource: String = "https://api.github.com/repos/samtkit/core/releases/latest"

@Parameter(names = ["--init"], description = "Downloads all required files and initializes the SAMT wrapper")
var init: Boolean = false

@Parameter(names = ["--init-source"], description = "The location from where the initial 'samtw', 'samtw.bat' and 'samt-wrapper.properties' will be downloaded from")
var initSource: String = "https://raw.githubusercontent.com/samtkit/core/main/wrapper"
}
49 changes: 35 additions & 14 deletions cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt
Original file line number Diff line number Diff line change
@@ -1,25 +1,46 @@
package tools.samt.cli

import tools.samt.common.DiagnosticContext
import tools.samt.common.DiagnosticController
import tools.samt.common.DiagnosticException
import tools.samt.common.SourceFile
import tools.samt.lexer.Lexer
import tools.samt.parser.FileNode
import tools.samt.parser.Parser
import tools.samt.semantic.SemanticModelBuilder

fun parseSourceFile(source: SourceFile, context: DiagnosticContext): FileNode? {
val tokenStream = Lexer.scan(source.content.reader(), context)
internal fun compile(command: CompileCommand, controller: DiagnosticController) {
val sourceFiles = command.files.readSamtSourceFiles(controller)

if (context.hasErrors()) {
return null
if (controller.hasErrors()) {
return
}

val fileNode = try {
Parser.parse(source, tokenStream, context)
} catch (e: DiagnosticException) {
// error message is added to the diagnostic console, so it can be ignored here
return null
// attempt to parse each source file into an AST
val fileNodes = buildList {
for (source in sourceFiles) {
val context = controller.createContext(source)
val tokenStream = Lexer.scan(source.content.reader(), context)

if (context.hasErrors()) {
continue
}

val fileNode = try {
Parser.parse(source, tokenStream, context)
} catch (e: DiagnosticException) {
// error message is added to the diagnostic console, so it can be ignored here
continue
}

add(fileNode)
}
}

return fileNode
}
// if any source files failed to parse, exit
if (controller.hasErrors()) {
return
}

// build up the semantic model from the AST
SemanticModelBuilder.build(fileNodes, controller)

// Code Generators will be called here
}
71 changes: 71 additions & 0 deletions cli/src/main/kotlin/tools/samt/cli/CliDumper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package tools.samt.cli

import com.github.ajalt.mordant.terminal.Terminal
import tools.samt.common.DiagnosticController
import tools.samt.common.DiagnosticException
import tools.samt.lexer.Lexer
import tools.samt.parser.Parser
import tools.samt.semantic.SemanticModelBuilder

internal fun dump(command: DumpCommand, terminal: Terminal, controller: DiagnosticController) {
val sourceFiles = command.files.readSamtSourceFiles(controller)

if (controller.hasErrors()) {
return
}

val dumpAll = !command.dumpTokens && !command.dumpAst && !command.dumpTypes

// attempt to parse each source file into an AST
val fileNodes = buildList {
for (source in sourceFiles) {
val context = controller.createContext(source)

if (dumpAll || command.dumpTokens) {
// create duplicate scan because sequence can only be iterated once
val tokenStream = Lexer.scan(source.content.reader(), context)
terminal.println("Tokens for ${source.absolutePath}:")
terminal.println(TokenPrinter.dump(tokenStream))
// clear the diagnostic messages so that messages aren't duplicated
context.messages.clear()
}

val tokenStream = Lexer.scan(source.content.reader(), context)

if (context.hasErrors()) {
continue
}

val fileNode = try {
Parser.parse(source, tokenStream, context)
} catch (e: DiagnosticException) {
// error message is added to the diagnostic console, so it can be ignored here
continue
}

if (dumpAll || command.dumpAst) {
terminal.println(ASTPrinter.dump(fileNode))
}

if (context.hasErrors()) {
continue
}

add(fileNode)
}
}

// if any source files failed to parse, exit
if (controller.hasErrors()) {
return
}

// build up the semantic model from the AST
SemanticModelBuilder.build(fileNodes, controller)

if (dumpAll || command.dumpTypes) {
terminal.println("Types:")
terminal.println("Not yet implemented")
// Type dumper will be added here
}
}
Loading