diff --git a/README.md b/README.md index e33d711..68516e9 100644 --- a/README.md +++ b/README.md @@ -5,28 +5,21 @@ A small BBQ language. Very WIP. Like VERY. ```pork -main = { - three = 3 - two = 2 - calculateSimple = { - (50 + three) * two - } - calculateComplex = { - three + two + 50 - } - calculateSimpleResult = calculateSimple() - calculateComplexResult = calculateComplex() - - list = [10, 20, 30] - trueValue = true - falseValue = false - - [ - calculateSimpleResult, - calculateComplexResult, - list, - trueValue, - falseValue - ] +/* fibonacci sequence */ +fib = { n in + if n == 0 + then 0 + else if n == 1 + then 1 + else fib(n - 1) + fib(n - 2) } + +result = fib(20) +println(result) +``` + +## Usage + +``` +./gradlew -q run --args 'run examples/fib.pork' ``` diff --git a/build.gradle.kts b/build.gradle.kts index 0625f8c..a711d5b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,6 +23,7 @@ java { dependencies { implementation("org.jetbrains.kotlin:kotlin-bom") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("com.github.ajalt.clikt:clikt:4.2.0") } tasks.withType { @@ -34,15 +35,17 @@ tasks.withType { } application { - mainClass.set("gay.pizza.pork.MainKt") + mainClass.set("gay.pizza.pork.cli.MainKt") } graalvmNative { binaries { named("main") { imageName.set("pork") - mainClass.set("gay.pizza.pork.MainKt") + mainClass.set("gay.pizza.pork.cli.MainKt") sharedLibrary.set(false) } } } + +tasks.run.get().outputs.upToDateWhen { false } diff --git a/examples/fib.pork b/examples/fib.pork index d3e943c..8a5490c 100644 --- a/examples/fib.pork +++ b/examples/fib.pork @@ -7,4 +7,5 @@ fib = { n in else fib(n - 1) + fib(n - 2) } -main = { in fib(20) } +result = fib(20) +println(result) diff --git a/examples/if.pork b/examples/if.pork deleted file mode 100644 index fc7fe83..0000000 --- a/examples/if.pork +++ /dev/null @@ -1,7 +0,0 @@ -check = { value in - if value then 50 -} - -main = { in - value = check(100) -} diff --git a/examples/syntax.pork b/examples/syntax.pork index bc70427..4f9d27c 100644 --- a/examples/syntax.pork +++ b/examples/syntax.pork @@ -1,53 +1,52 @@ -/* main function */ -main = { in - three = 3 - two = 2 +three = 3 +two = 2 - calculateSimple = { in - (50 + three) * two - } - - calculateComplex = { in - three + two + 50 - } - - multiply = { a, b in - a * b - } - - // calculates the result - calculateSimpleResult = calculateSimple() - calculateComplexResult = calculateComplex() - multiplyResult = multiply(50, 50) - - list = [10, 20, 30] - trueValue = true - falseValue = false - - invert = { value in - !value - } - - notEqual = { a, b in - a != b - } - - equal = { a, b in - a == b - } - - [ - calculateSimpleResult, - calculateComplexResult, - multiplyResult, - list, - trueValue, - falseValue, - invert(true), - invert(false), - equal(5, 5), - equal(5, 6), - notEqual(5, 5), - notEqual(5, 6) - ] +calculateSimple = { in + (50 + three) * two } + +calculateComplex = { in + three + two + 50 +} + +multiply = { a, b in + a * b +} + +// calculates the result +calculateSimpleResult = calculateSimple() +calculateComplexResult = calculateComplex() +multiplyResult = multiply(50, 50) + +list = [10, 20, 30] +trueValue = true +falseValue = false + +invert = { value in + !value +} + +notEqual = { a, b in + a != b +} + +equal = { a, b in + a == b +} + +results = [ + calculateSimpleResult, + calculateComplexResult, + multiplyResult, + list, + trueValue, + falseValue, + invert(true), + invert(false), + equal(5, 5), + equal(5, 6), + notEqual(5, 5), + notEqual(5, 6) +] + +println(results) diff --git a/src/main/kotlin/gay/pizza/pork/cli/GenerateKotlinCommand.kt b/src/main/kotlin/gay/pizza/pork/cli/GenerateKotlinCommand.kt new file mode 100644 index 0000000..446d152 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/cli/GenerateKotlinCommand.kt @@ -0,0 +1,23 @@ +package gay.pizza.pork.cli + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.types.path +import gay.pizza.pork.compiler.KotlinCompiler +import gay.pizza.pork.parse.PorkParser +import gay.pizza.pork.parse.PorkTokenizer +import gay.pizza.pork.parse.StringCharSource +import gay.pizza.pork.parse.TokenStreamSource +import kotlin.io.path.readText + +class GenerateKotlinCommand : CliktCommand(help = "Generate Kotlin Code", name = "generate-kotlin") { + val path by argument("file").path(mustExist = true, canBeDir = false) + + override fun run() { + val content = path.readText() + val tokenStream = PorkTokenizer(StringCharSource(content)).tokenize() + val program = PorkParser(TokenStreamSource(tokenStream)).readProgram() + val compiler = KotlinCompiler() + println(compiler.visit(program)) + } +} diff --git a/src/main/kotlin/gay/pizza/pork/cli/HighlightCommand.kt b/src/main/kotlin/gay/pizza/pork/cli/HighlightCommand.kt new file mode 100644 index 0000000..c1c6873 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/cli/HighlightCommand.kt @@ -0,0 +1,21 @@ +package gay.pizza.pork.cli + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.types.path +import gay.pizza.pork.parse.AnsiHighlightScheme +import gay.pizza.pork.parse.Highlighter +import gay.pizza.pork.parse.PorkTokenizer +import gay.pizza.pork.parse.StringCharSource +import kotlin.io.path.readText + +class HighlightCommand : CliktCommand(help = "Syntax Highlighter", name = "highlight") { + val path by argument("file").path(mustExist = true, canBeDir = false) + + override fun run() { + val content = path.readText() + val tokenStream = PorkTokenizer(StringCharSource(content)).tokenize() + val highlighter = Highlighter(AnsiHighlightScheme()) + print(highlighter.highlight(tokenStream).joinToString("")) + } +} diff --git a/src/main/kotlin/gay/pizza/pork/cli/RootCommand.kt b/src/main/kotlin/gay/pizza/pork/cli/RootCommand.kt new file mode 100644 index 0000000..3adc989 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/cli/RootCommand.kt @@ -0,0 +1,20 @@ +package gay.pizza.pork.cli + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.subcommands + +class RootCommand : CliktCommand( + help = "Pork - The BBQ Language", + name = "pork" +) { + init { + subcommands( + RunCommand(), + HighlightCommand(), + TokenizeCommand(), + GenerateKotlinCommand() + ) + } + + override fun run() {} +} diff --git a/src/main/kotlin/gay/pizza/pork/cli/RunCommand.kt b/src/main/kotlin/gay/pizza/pork/cli/RunCommand.kt new file mode 100644 index 0000000..7c4a7de --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/cli/RunCommand.kt @@ -0,0 +1,32 @@ +package gay.pizza.pork.cli + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.types.path +import gay.pizza.pork.eval.CallableFunction +import gay.pizza.pork.eval.PorkEvaluator +import gay.pizza.pork.eval.Scope +import gay.pizza.pork.parse.PorkParser +import gay.pizza.pork.parse.PorkTokenizer +import gay.pizza.pork.parse.StringCharSource +import gay.pizza.pork.parse.TokenStreamSource +import kotlin.io.path.readText + +class RunCommand : CliktCommand(help = "Run Program", name = "run") { + val path by argument("file").path(mustExist = true, canBeDir = false) + + override fun run() { + val content = path.readText() + val tokenStream = PorkTokenizer(StringCharSource(content)).tokenize() + val program = PorkParser(TokenStreamSource(tokenStream)).readProgram() + + val scope = Scope() + scope.define("println", CallableFunction { arguments -> + for (argument in arguments.values) { + println(argument) + } + }) + val evaluator = PorkEvaluator(scope) + evaluator.visit(program) + } +} diff --git a/src/main/kotlin/gay/pizza/pork/cli/TokenizeCommand.kt b/src/main/kotlin/gay/pizza/pork/cli/TokenizeCommand.kt new file mode 100644 index 0000000..4ac51d0 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/cli/TokenizeCommand.kt @@ -0,0 +1,25 @@ +package gay.pizza.pork.cli + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.types.path +import gay.pizza.pork.parse.PorkTokenizer +import gay.pizza.pork.parse.StringCharSource +import kotlin.io.path.readText + +class TokenizeCommand : CliktCommand(help = "Tokenize Program", name = "tokenize") { + val path by argument("file").path(mustExist = true, canBeDir = false) + + override fun run() { + val content = path.readText() + val tokenStream = PorkTokenizer(StringCharSource(content)).tokenize() + for (token in tokenStream.tokens) { + println("${token.start} ${token.type.name} '${sanitize(token.text)}'") + } + } + + private fun sanitize(input: String): String = + input + .replace("\n", "\\n") + .replace("\r", "\\r") +} diff --git a/src/main/kotlin/gay/pizza/pork/cli/main.kt b/src/main/kotlin/gay/pizza/pork/cli/main.kt new file mode 100644 index 0000000..6087412 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/cli/main.kt @@ -0,0 +1,3 @@ +package gay.pizza.pork.cli + +fun main(args: Array) = RootCommand().main(args) diff --git a/src/main/kotlin/gay/pizza/pork/main.kt b/src/main/kotlin/gay/pizza/pork/main.kt deleted file mode 100644 index d8377aa..0000000 --- a/src/main/kotlin/gay/pizza/pork/main.kt +++ /dev/null @@ -1,65 +0,0 @@ -package gay.pizza.pork - -import gay.pizza.pork.ast.Printer -import gay.pizza.pork.ast.nodes.Program -import gay.pizza.pork.eval.Arguments -import gay.pizza.pork.eval.PorkEvaluator -import gay.pizza.pork.eval.Scope -import gay.pizza.pork.parse.* -import kotlin.io.path.Path -import kotlin.io.path.readText - -fun eval(ast: Program) { - val scope = Scope() - val evaluator = PorkEvaluator(scope) - evaluator.visit(ast) - println("> ${scope.call("main", Arguments.Zero)}") -} - -fun validateTokenSoundness(input: String, stream: TokenStream) { - var expectedIndex = 0 - for (token in stream.tokens) { - if (token.start != expectedIndex) { - throw RuntimeException("Expected token to be at index $expectedIndex but was ${token.start}") - } - val slice = input.slice(token.start until token.start + token.text.length) - if (slice != token.text) { - throw RuntimeException( - "Expected index ${token.start} for length ${token.text.length} to" + - " equal '${token.text}' but was '$slice'") - } - expectedIndex += token.text.length - } -} - -fun main(args: Array) { - val code = Path(args[0]).readText() - val stream = tokenize(code) - println(stream.tokens.joinToString("\n")) - val program = parse(stream) - eval(program) - - val exactCode = stream.tokens.joinToString("") { it.text } - validateTokenSoundness(code, stream) - if (exactCode != code) { - throw RuntimeException("Token reconstruction didn't succeed.") - } - - val generated = buildString { Printer(this).visit(program) } - val parsedAst = parse(tokenize(generated)) - parse(tokenize(generated)) - println(generated) - - if (program != parsedAst) { - throw RuntimeException("Equality of parsed AST from printer was not proven.") - } - - val highlighter = Highlighter(AnsiHighlightScheme()) - println(highlighter.highlight(stream).joinToString("")) -} - -fun tokenize(input: String): TokenStream = - PorkTokenizer(StringCharSource(input)).tokenize() - -fun parse(stream: TokenStream): Program = - PorkParser(TokenStreamSource(stream)).readProgram() diff --git a/src/main/kotlin/gay/pizza/pork/parse/PorkParser.kt b/src/main/kotlin/gay/pizza/pork/parse/PorkParser.kt index 2844a34..1d0d0f1 100644 --- a/src/main/kotlin/gay/pizza/pork/parse/PorkParser.kt +++ b/src/main/kotlin/gay/pizza/pork/parse/PorkParser.kt @@ -108,7 +108,9 @@ class PorkParser(source: PeekableSource) { } else -> { - throw RuntimeException("Failed to parse token: ${token.type} '${token.text}' as expression") + throw RuntimeException( + "Failed to parse token: ${token.type} '${token.text}' as expression" + + " (index ${unsanitizedSource.currentIndex})") } }