commit 6494858f026a056986a72af9093b3f5c76ddc6b2 Author: Alex Zenla Date: Sat Aug 19 15:29:07 2023 -0700 Initial Commit diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..0ebcd99 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,17 @@ +name: build +on: [push] +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - name: Build with Gradle + uses: gradle/gradle-build-action@v2 + with: + arguments: build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d468ff9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.gradle/ +.vscode/ +.idea/ +build/ +out/ +/work +/kotlin-js-store +/.fleet/* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e906025 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Gay Pizza Specifications + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e33d711 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# pork + +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 + ] +} +``` diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..0625f8c --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,48 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + application + + kotlin("jvm") version "1.9.0" + kotlin("plugin.serialization") version "1.9.0" + + id("com.github.johnrengelman.shadow") version "8.1.1" + id("org.graalvm.buildtools.native") version "0.9.23" +} + +repositories { + mavenCentral() +} + +java { + val javaVersion = JavaVersion.toVersion(17) + sourceCompatibility = javaVersion + targetCompatibility = javaVersion +} + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-bom") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") +} + +tasks.withType { + kotlinOptions.jvmTarget = "17" +} + +tasks.withType { + gradleVersion = "8.3" +} + +application { + mainClass.set("gay.pizza.pork.MainKt") +} + +graalvmNative { + binaries { + named("main") { + imageName.set("pork") + mainClass.set("gay.pizza.pork.MainKt") + sharedLibrary.set(false) + } + } +} diff --git a/examples/syntax.pork b/examples/syntax.pork new file mode 100644 index 0000000..a33cc40 --- /dev/null +++ b/examples/syntax.pork @@ -0,0 +1,24 @@ +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 + ] +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..033e24c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ac72c34 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..fcb6fca --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..6689b85 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..13c123b --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "pork" diff --git a/src/main/kotlin/gay/pizza/pork/PorkLanguage.kt b/src/main/kotlin/gay/pizza/pork/PorkLanguage.kt new file mode 100644 index 0000000..a9cce21 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/PorkLanguage.kt @@ -0,0 +1,3 @@ +package gay.pizza.pork + +object PorkLanguage diff --git a/src/main/kotlin/gay/pizza/pork/ast/BooleanLiteral.kt b/src/main/kotlin/gay/pizza/pork/ast/BooleanLiteral.kt new file mode 100644 index 0000000..3ac3c7d --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/BooleanLiteral.kt @@ -0,0 +1,5 @@ +package gay.pizza.pork.ast + +class BooleanLiteral(val value: Boolean) : Expression { + override val type: NodeType = NodeType.BooleanLiteral +} diff --git a/src/main/kotlin/gay/pizza/pork/ast/Define.kt b/src/main/kotlin/gay/pizza/pork/ast/Define.kt new file mode 100644 index 0000000..44a4109 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/Define.kt @@ -0,0 +1,8 @@ +package gay.pizza.pork.ast + +class Define(val symbol: Symbol, val value: Expression) : Expression { + override val type: NodeType = NodeType.Define + + override fun visitChildren(visitor: Visitor): List = + listOf(visitor.visit(symbol), visitor.visit(value)) +} diff --git a/src/main/kotlin/gay/pizza/pork/ast/Expression.kt b/src/main/kotlin/gay/pizza/pork/ast/Expression.kt new file mode 100644 index 0000000..c4f5387 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/Expression.kt @@ -0,0 +1,3 @@ +package gay.pizza.pork.ast + +interface Expression : Node diff --git a/src/main/kotlin/gay/pizza/pork/ast/FunctionCall.kt b/src/main/kotlin/gay/pizza/pork/ast/FunctionCall.kt new file mode 100644 index 0000000..a2c699e --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/FunctionCall.kt @@ -0,0 +1,8 @@ +package gay.pizza.pork.ast + +class FunctionCall(val symbol: Symbol) : Expression { + override val type: NodeType = NodeType.FunctionCall + + override fun visitChildren(visitor: Visitor): List = + listOf(visitor.visit(symbol)) +} diff --git a/src/main/kotlin/gay/pizza/pork/ast/InfixOperation.kt b/src/main/kotlin/gay/pizza/pork/ast/InfixOperation.kt new file mode 100644 index 0000000..dc07ce4 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/InfixOperation.kt @@ -0,0 +1,8 @@ +package gay.pizza.pork.ast + +class InfixOperation(val left: Expression, val op: InfixOperator, val right: Expression) : Expression { + override val type: NodeType = NodeType.InfixOperation + + override fun visitChildren(visitor: Visitor): List = + listOf(visitor.visit(left), visitor.visit(right)) +} diff --git a/src/main/kotlin/gay/pizza/pork/ast/InfixOperator.kt b/src/main/kotlin/gay/pizza/pork/ast/InfixOperator.kt new file mode 100644 index 0000000..b2637ac --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/InfixOperator.kt @@ -0,0 +1,8 @@ +package gay.pizza.pork.ast + +enum class InfixOperator(val token: String) { + Plus("+"), + Minus("-"), + Multiply("*"), + Divide("/") +} diff --git a/src/main/kotlin/gay/pizza/pork/ast/IntLiteral.kt b/src/main/kotlin/gay/pizza/pork/ast/IntLiteral.kt new file mode 100644 index 0000000..ca27bd2 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/IntLiteral.kt @@ -0,0 +1,5 @@ +package gay.pizza.pork.ast + +class IntLiteral(val value: Int) : Expression { + override val type: NodeType = NodeType.IntLiteral +} diff --git a/src/main/kotlin/gay/pizza/pork/ast/Lambda.kt b/src/main/kotlin/gay/pizza/pork/ast/Lambda.kt new file mode 100644 index 0000000..653c8c0 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/Lambda.kt @@ -0,0 +1,10 @@ +package gay.pizza.pork.ast + +class Lambda(val expressions: List) : Expression { + constructor(vararg expressions: Expression) : this(listOf(*expressions)) + + override val type: NodeType = NodeType.Lambda + + override fun visitChildren(visitor: Visitor): List = + expressions.map { expression -> visitor.visit(expression) } +} diff --git a/src/main/kotlin/gay/pizza/pork/ast/ListLiteral.kt b/src/main/kotlin/gay/pizza/pork/ast/ListLiteral.kt new file mode 100644 index 0000000..0041afd --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/ListLiteral.kt @@ -0,0 +1,10 @@ +package gay.pizza.pork.ast + +class ListLiteral(val items: List) : Expression { + constructor(vararg items: Expression) : this(listOf(*items)) + + override val type: NodeType = NodeType.ListLiteral + + override fun visitChildren(visitor: Visitor): List = + items.map { visitor.visit(it) } +} diff --git a/src/main/kotlin/gay/pizza/pork/ast/Node.kt b/src/main/kotlin/gay/pizza/pork/ast/Node.kt new file mode 100644 index 0000000..8e557f0 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/Node.kt @@ -0,0 +1,6 @@ +package gay.pizza.pork.ast + +interface Node { + val type: NodeType + fun visitChildren(visitor: Visitor): List = emptyList() +} diff --git a/src/main/kotlin/gay/pizza/pork/ast/NodeType.kt b/src/main/kotlin/gay/pizza/pork/ast/NodeType.kt new file mode 100644 index 0000000..73b9713 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/NodeType.kt @@ -0,0 +1,37 @@ +package gay.pizza.pork.ast + +import gay.pizza.pork.ast.NodeTypeTrait.* + +enum class NodeType(val parent: NodeType? = null, vararg traits: NodeTypeTrait) { + Node, + Symbol(Node), + Expression(Node, Intermediate), + Program(Node), + IntLiteral(Expression, Literal), + BooleanLiteral(Expression, Literal), + ListLiteral(Expression, Literal), + Parentheses(Expression), + Define(Expression), + Lambda(Expression), + InfixOperation(Expression), + SymbolReference(Expression), + FunctionCall(Expression); + + val parents: Set + + init { + val calculatedParents = mutableListOf() + var self = this + while (true) { + calculatedParents.add(self) + if (self.parent != null) { + self = self.parent!! + } else { + break + } + } + parents = calculatedParents.toSet() + } + + fun isa(type: NodeType): Boolean = this == type || parents.contains(type) +} diff --git a/src/main/kotlin/gay/pizza/pork/ast/NodeTypeTrait.kt b/src/main/kotlin/gay/pizza/pork/ast/NodeTypeTrait.kt new file mode 100644 index 0000000..b031749 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/NodeTypeTrait.kt @@ -0,0 +1,6 @@ +package gay.pizza.pork.ast + +enum class NodeTypeTrait { + Intermediate, + Literal +} diff --git a/src/main/kotlin/gay/pizza/pork/ast/Parentheses.kt b/src/main/kotlin/gay/pizza/pork/ast/Parentheses.kt new file mode 100644 index 0000000..931ff5e --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/Parentheses.kt @@ -0,0 +1,8 @@ +package gay.pizza.pork.ast + +class Parentheses(val expression: Expression) : Expression { + override val type: NodeType = NodeType.Parentheses + + override fun visitChildren(visitor: Visitor): List = + listOf(visitor.visit(expression)) +} diff --git a/src/main/kotlin/gay/pizza/pork/ast/Printer.kt b/src/main/kotlin/gay/pizza/pork/ast/Printer.kt new file mode 100644 index 0000000..6778996 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/Printer.kt @@ -0,0 +1,100 @@ +package gay.pizza.pork.ast + +class Printer(private val buffer: StringBuilder) : Visitor { + private var indent = 0 + + private fun append(text: String) { + buffer.append(text) + } + + private fun appendLine() { + buffer.appendLine() + } + + private fun indent() { + repeat(indent) { + append(" ") + } + } + + override fun visitDefine(node: Define) { + visit(node.symbol) + append(" = ") + visit(node.value) + } + + override fun visitFunctionCall(node: FunctionCall) { + visit(node.symbol) + append("()") + } + + override fun visitReference(node: SymbolReference) { + visit(node.symbol) + } + + override fun visitSymbol(node: Symbol) { + append(node.id) + } + + override fun visitLambda(node: Lambda) { + append("{") + indent++ + for (expression in node.expressions) { + appendLine() + indent() + visit(expression) + } + + if (node.expressions.isNotEmpty()) { + appendLine() + } + indent-- + indent() + append("}") + } + + override fun visitIntLiteral(node: IntLiteral) { + append(node.value.toString()) + } + + override fun visitBooleanLiteral(node: BooleanLiteral) { + if (node.value) { + append("true") + } else { + append("false") + } + } + + override fun visitListLiteral(node: ListLiteral) { + append("[") + for ((index, item) in node.items.withIndex()) { + visit(item) + if (index != node.items.size - 1) { + append(", ") + } + } + append("]") + } + + override fun visitParentheses(node: Parentheses) { + append("(") + visit(node.expression) + append(")") + } + + override fun visitInfixOperation(node: InfixOperation) { + visit(node.left) + append(" ") + append(node.op.token) + append(" ") + visit(node.right) + } + + override fun visitProgram(node: Program) { + for (expression in node.expressions) { + indent() + visit(expression) + appendLine() + } + } +} diff --git a/src/main/kotlin/gay/pizza/pork/ast/Program.kt b/src/main/kotlin/gay/pizza/pork/ast/Program.kt new file mode 100644 index 0000000..6eca752 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/Program.kt @@ -0,0 +1,10 @@ +package gay.pizza.pork.ast + +class Program(val expressions: List) : Node { + constructor(vararg expressions: Expression) : this(listOf(*expressions)) + + override val type: NodeType = NodeType.Program + + override fun visitChildren(visitor: Visitor): List = + expressions.map { visitor.visit(it) } +} diff --git a/src/main/kotlin/gay/pizza/pork/ast/Symbol.kt b/src/main/kotlin/gay/pizza/pork/ast/Symbol.kt new file mode 100644 index 0000000..c0bd153 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/Symbol.kt @@ -0,0 +1,5 @@ +package gay.pizza.pork.ast + +class Symbol(val id: String) : Node { + override val type: NodeType = NodeType.Symbol +} diff --git a/src/main/kotlin/gay/pizza/pork/ast/SymbolReference.kt b/src/main/kotlin/gay/pizza/pork/ast/SymbolReference.kt new file mode 100644 index 0000000..2839570 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/SymbolReference.kt @@ -0,0 +1,8 @@ +package gay.pizza.pork.ast + +class SymbolReference(val symbol: Symbol) : Expression { + override val type: NodeType = NodeType.SymbolReference + + override fun visitChildren(visitor: Visitor): List = + listOf(visitor.visit(symbol)) +} diff --git a/src/main/kotlin/gay/pizza/pork/ast/Visitor.kt b/src/main/kotlin/gay/pizza/pork/ast/Visitor.kt new file mode 100644 index 0000000..22cbcee --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/ast/Visitor.kt @@ -0,0 +1,38 @@ +package gay.pizza.pork.ast + +interface Visitor { + fun visitDefine(node: Define): T + fun visitFunctionCall(node: FunctionCall): T + fun visitReference(node: SymbolReference): T + fun visitSymbol(node: Symbol): T + fun visitLambda(node: Lambda): T + + fun visitIntLiteral(node: IntLiteral): T + fun visitBooleanLiteral(node: BooleanLiteral): T + fun visitListLiteral(node: ListLiteral): T + + fun visitParentheses(node: Parentheses): T + fun visitInfixOperation(node: InfixOperation): T + + fun visitProgram(node: Program): T + + fun visitExpression(node: Expression): T = when (node) { + is IntLiteral -> visitIntLiteral(node) + is BooleanLiteral -> visitBooleanLiteral(node) + is ListLiteral -> visitListLiteral(node) + is Parentheses -> visitParentheses(node) + is InfixOperation -> visitInfixOperation(node) + is Define -> visitDefine(node) + is Lambda -> visitLambda(node) + is FunctionCall -> visitFunctionCall(node) + is SymbolReference -> visitReference(node) + else -> throw RuntimeException("Unknown Expression") + } + + fun visit(node: Node): T = when (node) { + is Expression -> visitExpression(node) + is Symbol -> visitSymbol(node) + is Program -> visitProgram(node) + else -> throw RuntimeException("Unknown Node") + } +} diff --git a/src/main/kotlin/gay/pizza/pork/eval/Context.kt b/src/main/kotlin/gay/pizza/pork/eval/Context.kt new file mode 100644 index 0000000..c759853 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/eval/Context.kt @@ -0,0 +1,46 @@ +package gay.pizza.pork.eval + +import java.util.function.Function + +class Context(val parent: Context? = null) { + private val variables = mutableMapOf() + + fun define(name: String, value: Any) { + if (variables.containsKey(name)) { + throw RuntimeException("Variable '${name}' is already defined.") + } + variables[name] = value + } + + fun value(name: String): Any { + val value = variables[name] + if (value == null) { + if (parent != null) { + return parent.value(name) + } + throw RuntimeException("Variable '${name}' not defined.") + } + return value + } + + fun call(name: String, argument: Any = Unit): Any { + val value = value(name) + if (value !is Function<*, *>) { + throw RuntimeException("$value is not callable.") + } + @Suppress("UNCHECKED_CAST") + val casted = value as Function + return casted.apply(argument) + } + + fun fork(): Context { + return Context(this) + } + + fun leave(): Context { + if (parent == null) { + throw RuntimeException("Parent context not found.") + } + return parent + } +} diff --git a/src/main/kotlin/gay/pizza/pork/eval/Evaluator.kt b/src/main/kotlin/gay/pizza/pork/eval/Evaluator.kt new file mode 100644 index 0000000..8501750 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/eval/Evaluator.kt @@ -0,0 +1,71 @@ +package gay.pizza.pork.eval + +import gay.pizza.pork.ast.* +import java.util.function.Function + +class Evaluator(root: Context) : Visitor { + private var currentContext: Context = root + + override fun visitDefine(node: Define): Any { + val value = visit(node.value) + currentContext.define(node.symbol.id, value) + return value + } + + override fun visitFunctionCall(node: FunctionCall): Any = currentContext.call(node.symbol.id) + + override fun visitReference(node: SymbolReference): Any = + currentContext.value(node.symbol.id) + + override fun visitSymbol(node: Symbol): Any { + return Unit + } + + override fun visitLambda(node: Lambda): Function { + return Function { _ -> + currentContext = currentContext.fork() + try { + var value: Any? = null + for (expression in node.expressions) { + value = visit(expression) + } + value ?: Unit + } finally { + currentContext = currentContext.leave() + } + } + } + + override fun visitIntLiteral(node: IntLiteral): Any = node.value + override fun visitBooleanLiteral(node: BooleanLiteral): Any = node.value + override fun visitListLiteral(node: ListLiteral): Any = node.items.map { visit(it) } + + override fun visitParentheses(node: Parentheses): Any = visit(node.expression) + + override fun visitInfixOperation(node: InfixOperation): Any { + val left = visit(node.left) + val right = visit(node.right) + + if (left !is Number || right !is Number) { + throw RuntimeException("Failed to evaluate infix operation, bad types.") + } + + val leftInt = left.toInt() + val rightInt = right.toInt() + + return when (node.op) { + InfixOperator.Plus -> leftInt + rightInt + InfixOperator.Minus -> leftInt - rightInt + InfixOperator.Multiply -> leftInt * rightInt + InfixOperator.Divide -> leftInt / rightInt + } + } + + override fun visitProgram(node: Program): Any { + var value: Any? = null + for (expression in node.expressions) { + value = visit(expression) + } + return value ?: Unit + } +} diff --git a/src/main/kotlin/gay/pizza/pork/main.kt b/src/main/kotlin/gay/pizza/pork/main.kt new file mode 100644 index 0000000..a28201b --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/main.kt @@ -0,0 +1,25 @@ +package gay.pizza.pork + +import gay.pizza.pork.ast.* +import gay.pizza.pork.eval.Context +import gay.pizza.pork.eval.Evaluator +import gay.pizza.pork.parse.* +import kotlin.io.path.Path +import kotlin.io.path.readText + +fun main(args: Array) { + fun eval(ast: Program) { + val context = Context() + val evaluator = Evaluator(context) + evaluator.visit(ast) + println("> ${context.call("main")}") + } + + val code = Path(args[0]).readText() + val tokenizer = PorkTokenizer(StringCharSource(code)) + val stream = tokenizer.tokenize() + println(stream.tokens.joinToString("\n")) + val parser = PorkParser(TokenStreamSource(stream)) + val program = parser.readProgram() + eval(program) +} diff --git a/src/main/kotlin/gay/pizza/pork/parse/CharSource.kt b/src/main/kotlin/gay/pizza/pork/parse/CharSource.kt new file mode 100644 index 0000000..a2253ce --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/parse/CharSource.kt @@ -0,0 +1,8 @@ +package gay.pizza.pork.parse + +interface CharSource : PeekableSource { + companion object { + @Suppress("ConstPropertyName") + const val NullChar = 0.toChar() + } +} diff --git a/src/main/kotlin/gay/pizza/pork/parse/PeekableSource.kt b/src/main/kotlin/gay/pizza/pork/parse/PeekableSource.kt new file mode 100644 index 0000000..6335c4c --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/parse/PeekableSource.kt @@ -0,0 +1,7 @@ +package gay.pizza.pork.parse + +interface PeekableSource { + val currentIndex: Int + fun next(): T + fun peek(): T +} diff --git a/src/main/kotlin/gay/pizza/pork/parse/PorkParser.kt b/src/main/kotlin/gay/pizza/pork/parse/PorkParser.kt new file mode 100644 index 0000000..a52ac99 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/parse/PorkParser.kt @@ -0,0 +1,125 @@ +package gay.pizza.pork.parse + +import gay.pizza.pork.ast.* + +class PorkParser(val source: PeekableSource) { + private fun readIntLiteral(): IntLiteral { + val token = expect(TokenType.IntLiteral) + return IntLiteral(token.text.toInt()) + } + + private fun readSymbol(): Symbol { + val token = expect(TokenType.Symbol) + return Symbol(token.text) + } + + private fun readSymbolCases(): Expression { + val symbol = readSymbol() + return if (peekType(TokenType.LeftParentheses)) { + expect(TokenType.LeftParentheses) + expect(TokenType.RightParentheses) + FunctionCall(symbol) + } else if (peekType(TokenType.Equals)) { + expect(TokenType.Equals) + Define(symbol, readExpression()) + } else { + SymbolReference(symbol) + } + } + + fun readLambda(): Lambda { + expect(TokenType.LeftCurly) + val items = collectExpressions(TokenType.RightCurly) + expect(TokenType.RightCurly) + return Lambda(items) + } + + fun readExpression(): Expression { + val token = source.peek() + val expression = when (token.type) { + TokenType.IntLiteral -> { + readIntLiteral() + } + TokenType.LeftBracket -> { + readListLiteral() + } + TokenType.Symbol -> { + readSymbolCases() + } + TokenType.LeftCurly -> { + readLambda() + } + TokenType.LeftParentheses -> { + expect(TokenType.LeftParentheses) + val expression = readExpression() + expect(TokenType.RightParentheses) + Parentheses(expression) + } + TokenType.True -> { + expect(TokenType.True) + return BooleanLiteral(true) + } + TokenType.False -> { + expect(TokenType.False) + return BooleanLiteral(false) + } + else -> { + throw RuntimeException("Failed to parse token: ${token.type} '${token.text}' as expression.") + } + } + + if (peekType(TokenType.Plus, TokenType.Minus, TokenType.Multiply, TokenType.Divide)) { + val infixToken = source.next() + val infixOperator = convertInfixOperator(infixToken) + return InfixOperation(expression, infixOperator, readExpression()) + } + return expression + } + + private fun convertInfixOperator(token: Token): InfixOperator = + when (token.type) { + TokenType.Plus -> InfixOperator.Plus + TokenType.Minus -> InfixOperator.Minus + TokenType.Multiply -> InfixOperator.Multiply + TokenType.Divide -> InfixOperator.Divide + else -> throw RuntimeException("Unknown Infix Operator") + } + + fun readListLiteral(): ListLiteral { + expect(TokenType.LeftBracket) + val items = collectExpressions(TokenType.RightBracket, TokenType.Comma) + expect(TokenType.RightBracket) + return ListLiteral(items) + } + + fun readProgram(): Program { + val items = collectExpressions(TokenType.EndOfFile) + expect(TokenType.EndOfFile) + return Program(items) + } + + private fun collectExpressions(peeking: TokenType, consuming: TokenType? = null): List { + val items = mutableListOf() + while (!peekType(peeking)) { + val expression = readExpression() + if (consuming != null && !peekType(peeking)) { + expect(consuming) + } + items.add(expression) + } + return items + } + + private fun peekType(vararg types: TokenType): Boolean { + val token = source.peek() + return types.contains(token.type) + } + + private fun expect(type: TokenType): Token { + val token = source.next() + if (token.type != type) { + throw RuntimeException("Expected token type '${type}' but got type ${token.type} '${token.text}'") + } + return token + } +} diff --git a/src/main/kotlin/gay/pizza/pork/parse/PorkTokenizer.kt b/src/main/kotlin/gay/pizza/pork/parse/PorkTokenizer.kt new file mode 100644 index 0000000..fe849d2 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/parse/PorkTokenizer.kt @@ -0,0 +1,87 @@ +package gay.pizza.pork.parse + +class PorkTokenizer(val source: CharSource) { + private var tokenStart: Int = 0 + + private fun isSymbol(c: Char): Boolean = + (c in 'a'..'z') || (c in 'A'..'Z') || c == '_' + + private fun isDigit(c: Char): Boolean = + c in '0'..'9' + + private fun isWhitespace(c: Char): Boolean = + c == ' ' || c == '\r' || c == '\n' || c == '\t' + + private fun readSymbolOrKeyword(firstChar: Char): Token { + val symbol = buildString { + append(firstChar) + while (isSymbol(source.peek())) { + append(source.next()) + } + } + + var type = TokenType.Symbol + for (keyword in TokenType.Keywords) { + if (symbol == keyword.keyword) { + type = keyword + } + } + + return Token(type, symbol) + } + + private fun readIntLiteral(firstChar: Char): Token { + val number = buildString { + append(firstChar) + while (isDigit(source.peek())) { + append(source.next()) + } + } + return Token(TokenType.IntLiteral, number) + } + + private fun skipWhitespace() { + while (isWhitespace(source.peek())) { + source.next() + } + } + + fun next(): Token { + while (source.peek() != CharSource.NullChar) { + tokenStart = source.currentIndex + val char = source.next() + for (item in TokenType.SingleChars) { + if (item.singleChar == char) { + return Token(item, char.toString()) + } + } + + if (isWhitespace(char)) { + skipWhitespace() + continue + } + + if (isDigit(char)) { + return readIntLiteral(char) + } + + if (isSymbol(char)) { + return readSymbolOrKeyword(char) + } + throw RuntimeException("Failed to parse: (${char}) next ${source.peek()}") + } + return TokenSource.EndOfFile + } + + fun tokenize(): TokenStream { + val tokens = mutableListOf() + while (true) { + val token = next() + tokens.add(token) + if (token.type == TokenType.EndOfFile) { + break + } + } + return TokenStream(tokens) + } +} diff --git a/src/main/kotlin/gay/pizza/pork/parse/StringCharSource.kt b/src/main/kotlin/gay/pizza/pork/parse/StringCharSource.kt new file mode 100644 index 0000000..6d7d996 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/parse/StringCharSource.kt @@ -0,0 +1,22 @@ +package gay.pizza.pork.parse + +class StringCharSource(val input: String) : CharSource { + private var index = 0 + override val currentIndex: Int = index + + override fun next(): Char { + if (index == input.length) { + return CharSource.NullChar + } + val char = input[index] + index++ + return char + } + + override fun peek(): Char { + if (index == input.length) { + return CharSource.NullChar + } + return input[index] + } +} diff --git a/src/main/kotlin/gay/pizza/pork/parse/Token.kt b/src/main/kotlin/gay/pizza/pork/parse/Token.kt new file mode 100644 index 0000000..ba87ee9 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/parse/Token.kt @@ -0,0 +1,5 @@ +package gay.pizza.pork.parse + +class Token(val type: TokenType, val text: String) { + override fun toString(): String = "${type.name} $text" +} diff --git a/src/main/kotlin/gay/pizza/pork/parse/TokenSource.kt b/src/main/kotlin/gay/pizza/pork/parse/TokenSource.kt new file mode 100644 index 0000000..eebf28e --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/parse/TokenSource.kt @@ -0,0 +1,7 @@ +package gay.pizza.pork.parse + +interface TokenSource : PeekableSource { + companion object { + val EndOfFile = Token(TokenType.EndOfFile, "") + } +} diff --git a/src/main/kotlin/gay/pizza/pork/parse/TokenStream.kt b/src/main/kotlin/gay/pizza/pork/parse/TokenStream.kt new file mode 100644 index 0000000..b55fd5b --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/parse/TokenStream.kt @@ -0,0 +1,5 @@ +package gay.pizza.pork.parse + +class TokenStream(val tokens: List) { + override fun toString(): String = tokens.toString() +} diff --git a/src/main/kotlin/gay/pizza/pork/parse/TokenStreamSource.kt b/src/main/kotlin/gay/pizza/pork/parse/TokenStreamSource.kt new file mode 100644 index 0000000..3719415 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/parse/TokenStreamSource.kt @@ -0,0 +1,22 @@ +package gay.pizza.pork.parse + +class TokenStreamSource(val stream: TokenStream) : TokenSource { + private var index = 0 + override val currentIndex: Int = index + + override fun next(): Token { + if (index == stream.tokens.size) { + return TokenSource.EndOfFile + } + val char = stream.tokens[index] + index++ + return char + } + + override fun peek(): Token { + if (index == stream.tokens.size) { + return TokenSource.EndOfFile + } + return stream.tokens[index] + } +} diff --git a/src/main/kotlin/gay/pizza/pork/parse/TokenType.kt b/src/main/kotlin/gay/pizza/pork/parse/TokenType.kt new file mode 100644 index 0000000..578cf48 --- /dev/null +++ b/src/main/kotlin/gay/pizza/pork/parse/TokenType.kt @@ -0,0 +1,26 @@ +package gay.pizza.pork.parse + +enum class TokenType(val singleChar: Char? = null, val keyword: String? = null) { + Symbol, + IntLiteral, + Equals(singleChar = '='), + Plus(singleChar = '+'), + Minus(singleChar = '-'), + Multiply(singleChar = '*'), + Divide(singleChar = '/'), + LeftCurly(singleChar = '{'), + RightCurly(singleChar = '}'), + LeftBracket(singleChar = '['), + RightBracket(singleChar = ']'), + LeftParentheses(singleChar = '('), + RightParentheses(singleChar = ')'), + Comma(singleChar = ','), + False(keyword = "false"), + True(keyword = "true"), + EndOfFile; + + companion object { + val Keywords = entries.filter { it.keyword != null } + val SingleChars = entries.filter { it.singleChar != null } + } +}