summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitattributes1
-rw-r--r--.gitignore39
-rw-r--r--build.gradle.kts47
-rw-r--r--dice.orang47
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin0 -> 60756 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xgradlew234
-rw-r--r--gradlew.bat89
-rw-r--r--grammar.bnf29
-rw-r--r--settings.gradle.kts2
-rw-r--r--src/main/java/lv/enes/orang/Builtins.java95
-rw-r--r--src/main/java/lv/enes/orang/Codepoint.java24
-rw-r--r--src/main/java/lv/enes/orang/Lexer.java168
-rw-r--r--src/main/java/lv/enes/orang/Main.java77
-rw-r--r--src/main/java/lv/enes/orang/NonEmptyList.java23
-rw-r--r--src/main/java/lv/enes/orang/OrangException.java11
-rw-r--r--src/main/java/lv/enes/orang/OrangRuntimeException.java11
-rw-r--r--src/main/java/lv/enes/orang/Parser.java291
-rw-r--r--src/main/java/lv/enes/orang/ParserException.java7
-rw-r--r--src/main/java/lv/enes/orang/PeekableStream.java38
-rw-r--r--src/main/java/lv/enes/orang/Scope.java54
-rw-r--r--src/main/java/lv/enes/orang/State.java12
-rw-r--r--src/main/java/lv/enes/orang/Token.java15
-rw-r--r--src/main/java/lv/enes/orang/TokenType.java91
-rw-r--r--src/main/java/lv/enes/orang/ast/ArrayExpression.java21
-rw-r--r--src/main/java/lv/enes/orang/ast/BinaryExpression.java53
-rw-r--r--src/main/java/lv/enes/orang/ast/BooleanLiteral.java12
-rw-r--r--src/main/java/lv/enes/orang/ast/CallExpression.java12
-rw-r--r--src/main/java/lv/enes/orang/ast/DefSpec.java6
-rw-r--r--src/main/java/lv/enes/orang/ast/Definition.java15
-rw-r--r--src/main/java/lv/enes/orang/ast/DoExpression.java19
-rw-r--r--src/main/java/lv/enes/orang/ast/Expression.java14
-rw-r--r--src/main/java/lv/enes/orang/ast/FnExpression.java14
-rw-r--r--src/main/java/lv/enes/orang/ast/IfElseExpression.java22
-rw-r--r--src/main/java/lv/enes/orang/ast/IntLiteral.java12
-rw-r--r--src/main/java/lv/enes/orang/ast/LetInExpression.java27
-rw-r--r--src/main/java/lv/enes/orang/ast/Program.java17
-rw-r--r--src/main/java/lv/enes/orang/ast/Statement.java8
-rw-r--r--src/main/java/lv/enes/orang/ast/StringLiteral.java13
-rw-r--r--src/main/java/lv/enes/orang/ast/UnaryExpression.java23
-rw-r--r--src/main/java/lv/enes/orang/ast/VariableExpression.java12
-rw-r--r--src/main/java/lv/enes/orang/value/Array.java65
-rw-r--r--src/main/java/lv/enes/orang/value/BuiltinFunction.java31
-rw-r--r--src/main/java/lv/enes/orang/value/Function.java28
-rw-r--r--src/main/java/lv/enes/orang/value/OrangBoolean.java55
-rw-r--r--src/main/java/lv/enes/orang/value/OrangInteger.java70
-rw-r--r--src/main/java/lv/enes/orang/value/OrangString.java49
-rw-r--r--src/main/java/lv/enes/orang/value/PartialBuiltinFunction.java32
-rw-r--r--src/main/java/lv/enes/orang/value/PartialFunction.java37
-rw-r--r--src/main/java/lv/enes/orang/value/Undefined.java21
-rw-r--r--src/main/java/lv/enes/orang/value/Value.java77
-rw-r--r--src/main/java/module-info.java9
-rw-r--r--src/main/resources/lv/enes/orang/prelude.orang20
53 files changed, 2205 insertions, 0 deletions
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..6e6194a
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
*.bat text=auto eol=crlf \ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f35ca9e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,39 @@
1.gradle
2build/
3!gradle/wrapper/gradle-wrapper.jar
4!**/src/main/**/build/
5!**/src/test/**/build/
6
7### IntelliJ IDEA ###
8.idea/
9*.iws
10*.iml
11*.ipr
12out/
13!**/src/main/**/out/
14!**/src/test/**/out/
15
16### Eclipse ###
17.apt_generated
18.classpath
19.factorypath
20.project
21.settings
22.springBeans
23.sts4-cache
24bin/
25!**/src/main/**/bin/
26!**/src/test/**/bin/
27
28### NetBeans ###
29/nbproject/private/
30/nbbuild/
31/dist/
32/nbdist/
33/.nb-gradle/
34
35### VS Code ###
36.vscode/
37
38### Mac OS ###
39.DS_Store \ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..212943a
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,47 @@
1plugins {
2 application
3 java
4 id("io.freefair.lombok") version "8.6"
5 id("org.beryx.jlink") version "3.0.1"
6}
7
8val slf4jVersion = "2.0.13"
9val jakartaAnnotationVersion = "3.0.0"
10
11group = "lv.enes"
12version = "0.1-SNAPSHOT"
13
14repositories {
15 mavenCentral()
16}
17
18dependencies {
19 implementation("jakarta.annotation:jakarta.annotation-api:$jakartaAnnotationVersion")
20 implementation("org.slf4j:slf4j-api:$slf4jVersion")
21 implementation("org.slf4j:slf4j-simple:$slf4jVersion")
22}
23
24java {
25 sourceCompatibility = JavaVersion.VERSION_22
26 targetCompatibility = JavaVersion.VERSION_22
27 toolchain {
28 languageVersion = JavaLanguageVersion.of(22)
29 }
30}
31
32tasks.withType<JavaCompile> {
33 options.compilerArgs.add("--enable-preview")
34}
35
36application {
37 mainModule = "lv.enes.orang"
38 mainClass = "lv.enes.orang.Main"
39}
40
41jlink {
42 options.set(listOf("--strip-debug", "--compress", "2", "--no-header-files", "--no-man-pages"))
43 launcher {
44 name = "orang"
45 jvmArgs = listOf("--enable-preview")
46 }
47} \ No newline at end of file
diff --git a/dice.orang b/dice.orang
new file mode 100644
index 0000000..0fae27b
--- /dev/null
+++ b/dice.orang
@@ -0,0 +1,47 @@
1def dice sides times =
2 let once _ = randInt 1 (sides + 1) in
3 let aux acc times =
4 if times ?= 0 then acc
5 else aux (acc + once ()) (times - 1)
6 in
7 aux 0 times
8
9def guessing_game _ =
10 let secret = randInt 1 101
11 and guessOnce _ = do
12 print "Make your guess: ";
13 let guess = readInt () in
14 if secret > guess then do
15 printLn "My number is larger than that...";
16 false
17 end else if secret < guess then do
18 printLn "My number is smaller than that...";
19 false
20 end else
21 true
22 end
23 and shouldEnd times =
24 if guessOnce () then do
25 printLn "Congratulations! You guessed right!";
26 true
27 end else if times ?= 1 then do
28 printLn "That was your last guess...";
29 true
30 end else do
31 printLn "Guess again...";
32 false
33 end
34 and aux times = do
35 print "You have "; print times; printLn " guesses left";
36 if shouldEnd times then do
37 print "My number was "; printLn secret
38 end else
39 aux (times - 1)
40 end in do
41 printLn "Welcome to the number guessing game";
42 printLn "I will pick a random number between 1 and 100 (inclusive)";
43 printLn " and you try to guess it.";
44 printLn "After each guess, I will tell you if my number was larger or smaller";
45 printLn "Good Luck and Have Fun!";
46 aux 10
47 end
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..249e583
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..78f6950
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
1#Wed Jul 31 14:07:13 EEST 2024
2distributionBase=GRADLE_USER_HOME
3distributionPath=wrapper/dists
4distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
5zipStoreBase=GRADLE_USER_HOME
6zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..1b6c787
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,234 @@
1#!/bin/sh
2
3#
4# Copyright © 2015-2021 the original authors.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# https://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18
19##############################################################################
20#
21# Gradle start up script for POSIX generated by Gradle.
22#
23# Important for running:
24#
25# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26# noncompliant, but you have some other compliant shell such as ksh or
27# bash, then to run this script, type that shell name before the whole
28# command line, like:
29#
30# ksh Gradle
31#
32# Busybox and similar reduced shells will NOT work, because this script
33# requires all of these POSIX shell features:
34# * functions;
35# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37# * compound commands having a testable exit status, especially «case»;
38# * various built-in commands including «command», «set», and «ulimit».
39#
40# Important for patching:
41#
42# (2) This script targets any POSIX shell, so it avoids extensions provided
43# by Bash, Ksh, etc; in particular arrays are avoided.
44#
45# The "traditional" practice of packing multiple parameters into a
46# space-separated string is a well documented source of bugs and security
47# problems, so this is (mostly) avoided, by progressively accumulating
48# options in "$@", and eventually passing that to Java.
49#
50# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52# see the in-line comments for details.
53#
54# There are tweaks for specific operating systems such as AIX, CygWin,
55# Darwin, MinGW, and NonStop.
56#
57# (3) This script is generated from the Groovy template
58# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59# within the Gradle project.
60#
61# You can find Gradle at https://github.com/gradle/gradle/.
62#
63##############################################################################
64
65# Attempt to set APP_HOME
66
67# Resolve links: $0 may be a link
68app_path=$0
69
70# Need this for daisy-chained symlinks.
71while
72 APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 [ -h "$app_path" ]
74do
75 ls=$( ls -ld "$app_path" )
76 link=${ls#*' -> '}
77 case $link in #(
78 /*) app_path=$link ;; #(
79 *) app_path=$APP_HOME$link ;;
80 esac
81done
82
83APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84
85APP_NAME="Gradle"
86APP_BASE_NAME=${0##*/}
87
88# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90
91# Use the maximum available, or set MAX_FD != -1 to use that value.
92MAX_FD=maximum
93
94warn () {
95 echo "$*"
96} >&2
97
98die () {
99 echo
100 echo "$*"
101 echo
102 exit 1
103} >&2
104
105# OS specific support (must be 'true' or 'false').
106cygwin=false
107msys=false
108darwin=false
109nonstop=false
110case "$( uname )" in #(
111 CYGWIN* ) cygwin=true ;; #(
112 Darwin* ) darwin=true ;; #(
113 MSYS* | MINGW* ) msys=true ;; #(
114 NONSTOP* ) nonstop=true ;;
115esac
116
117CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118
119
120# Determine the Java command to use to start the JVM.
121if [ -n "$JAVA_HOME" ] ; then
122 if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 # IBM's JDK on AIX uses strange locations for the executables
124 JAVACMD=$JAVA_HOME/jre/sh/java
125 else
126 JAVACMD=$JAVA_HOME/bin/java
127 fi
128 if [ ! -x "$JAVACMD" ] ; then
129 die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130
131Please set the JAVA_HOME variable in your environment to match the
132location of your Java installation."
133 fi
134else
135 JAVACMD=java
136 which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137
138Please set the JAVA_HOME variable in your environment to match the
139location of your Java installation."
140fi
141
142# Increase the maximum file descriptors if we can.
143if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 case $MAX_FD in #(
145 max*)
146 MAX_FD=$( ulimit -H -n ) ||
147 warn "Could not query maximum file descriptor limit"
148 esac
149 case $MAX_FD in #(
150 '' | soft) :;; #(
151 *)
152 ulimit -n "$MAX_FD" ||
153 warn "Could not set maximum file descriptor limit to $MAX_FD"
154 esac
155fi
156
157# Collect all arguments for the java command, stacking in reverse order:
158# * args from the command line
159# * the main class name
160# * -classpath
161# * -D...appname settings
162# * --module-path (only if needed)
163# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164
165# For Cygwin or MSYS, switch paths to Windows format before running java
166if "$cygwin" || "$msys" ; then
167 APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169
170 JAVACMD=$( cygpath --unix "$JAVACMD" )
171
172 # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 for arg do
174 if
175 case $arg in #(
176 -*) false ;; # don't mess with options #(
177 /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 [ -e "$t" ] ;; #(
179 *) false ;;
180 esac
181 then
182 arg=$( cygpath --path --ignore --mixed "$arg" )
183 fi
184 # Roll the args list around exactly as many times as the number of
185 # args, so each arg winds up back in the position where it started, but
186 # possibly modified.
187 #
188 # NB: a `for` loop captures its iteration list before it begins, so
189 # changing the positional parameters here affects neither the number of
190 # iterations, nor the values presented in `arg`.
191 shift # remove old arg
192 set -- "$@" "$arg" # push replacement arg
193 done
194fi
195
196# Collect all arguments for the java command;
197# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198# shell script including quotes and variable substitutions, so put them in
199# double quotes to make sure that they get re-expanded; and
200# * put everything else in single quotes, so that it's not re-expanded.
201
202set -- \
203 "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 -classpath "$CLASSPATH" \
205 org.gradle.wrapper.GradleWrapperMain \
206 "$@"
207
208# Use "xargs" to parse quoted args.
209#
210# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211#
212# In Bash we could simply go:
213#
214# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215# set -- "${ARGS[@]}" "$@"
216#
217# but POSIX shell has neither arrays nor command substitution, so instead we
218# post-process each arg (as a line of input to sed) to backslash-escape any
219# character that might be a shell metacharacter, then use eval to reverse
220# that process (while maintaining the separation between arguments), and wrap
221# the whole thing up as a single "set" statement.
222#
223# This will of course break if any of these variables contains a newline or
224# an unmatched quote.
225#
226
227eval "set -- $(
228 printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 xargs -n1 |
230 sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 tr '\n' ' '
232 )" '"$@"'
233
234exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
1@rem
2@rem Copyright 2015 the original author or authors.
3@rem
4@rem Licensed under the Apache License, Version 2.0 (the "License");
5@rem you may not use this file except in compliance with the License.
6@rem You may obtain a copy of the License at
7@rem
8@rem https://www.apache.org/licenses/LICENSE-2.0
9@rem
10@rem Unless required by applicable law or agreed to in writing, software
11@rem distributed under the License is distributed on an "AS IS" BASIS,
12@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13@rem See the License for the specific language governing permissions and
14@rem limitations under the License.
15@rem
16
17@if "%DEBUG%" == "" @echo off
18@rem ##########################################################################
19@rem
20@rem Gradle startup script for Windows
21@rem
22@rem ##########################################################################
23
24@rem Set local scope for the variables with windows NT shell
25if "%OS%"=="Windows_NT" setlocal
26
27set DIRNAME=%~dp0
28if "%DIRNAME%" == "" set DIRNAME=.
29set APP_BASE_NAME=%~n0
30set APP_HOME=%DIRNAME%
31
32@rem Resolve any "." and ".." in APP_HOME to make it shorter.
33for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34
35@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37
38@rem Find java.exe
39if defined JAVA_HOME goto findJavaFromJavaHome
40
41set JAVA_EXE=java.exe
42%JAVA_EXE% -version >NUL 2>&1
43if "%ERRORLEVEL%" == "0" goto execute
44
45echo.
46echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47echo.
48echo Please set the JAVA_HOME variable in your environment to match the
49echo location of your Java installation.
50
51goto fail
52
53:findJavaFromJavaHome
54set JAVA_HOME=%JAVA_HOME:"=%
55set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56
57if exist "%JAVA_EXE%" goto execute
58
59echo.
60echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61echo.
62echo Please set the JAVA_HOME variable in your environment to match the
63echo location of your Java installation.
64
65goto fail
66
67:execute
68@rem Setup the command line
69
70set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71
72
73@rem Execute Gradle
74"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75
76:end
77@rem End local scope for the variables with windows NT shell
78if "%ERRORLEVEL%"=="0" goto mainEnd
79
80:fail
81rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82rem the _cmd.exe /c_ return code!
83if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84exit /b 1
85
86:mainEnd
87if "%OS%"=="Windows_NT" endlocal
88
89:omega
diff --git a/grammar.bnf b/grammar.bnf
new file mode 100644
index 0000000..256e58a
--- /dev/null
+++ b/grammar.bnf
@@ -0,0 +1,29 @@
1program ::= (statement ';'?)*;
2statement ::= definition | expression;
3definition ::= 'def' def-spec '=' expression;
4def-spec ::= IDENTIFIER arg-spec*;
5arg-spec ::= IDENTIFIER;
6expression ::= binary-expression | unary-expression;
7unary-expression ::= unop+ simple-expression;
8binary-expression ::= call-expression (binop call-expression)*;
9call-expression ::= simple-expression+;
10binop ::= '*' | '/' | '+' | '-' | '?=' | '/=' | '>' | '>=' | '<' | '<=';
11unop ::= '+' | '-' | '!';
12simple-expression ::= '(' expression ')'
13 | 'true' | 'false'
14 | INTEGER
15 | IDENTIFIER
16 | STRING
17 | array
18 | if-else-expression
19 | let-in-expression
20 | fn-expression
21 | do-expression;
22
23array ::= '(' ')' | '[' ']' | '[' expression (',' expression)* ','? ']';
24
25if-else-expression ::= 'if' expression 'then' expression 'else' expression;
26let-in-expression ::= 'let' def-spec '=' expression ('and' def-spec '=' expression)* 'in' expression;
27fn-expression ::= 'fn' arg-spec+ '->' expression
28 | 'fn' arg-spec+ do-expression;
29do-expression ::= 'do' expression (';' expression)* 'end';
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..4c4ac7d
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,2 @@
1rootProject.name = "orang"
2
diff --git a/src/main/java/lv/enes/orang/Builtins.java b/src/main/java/lv/enes/orang/Builtins.java
new file mode 100644
index 0000000..0a76387
--- /dev/null
+++ b/src/main/java/lv/enes/orang/Builtins.java
@@ -0,0 +1,95 @@
1package lv.enes.orang;
2
3import lombok.extern.slf4j.Slf4j;
4import lv.enes.orang.value.*;
5
6import java.io.IOException;
7import java.util.List;
8import java.util.Map;
9import java.util.Random;
10
11import static lv.enes.orang.State.STDIN;
12import static lv.enes.orang.State.STDOUT;
13
14@Slf4j
15public final class Builtins {
16 public static final Map<String, Value> BUILTINS = Map.ofEntries(
17 Map.entry("__builtin_isRepl", new BuiltinFunction(1, Builtins::isRepl)),
18 Map.entry("__builtin_len", new BuiltinFunction(1, Builtins::len)),
19 Map.entry("__builtin_parseInt", new BuiltinFunction(1, Builtins::parseInt)),
20 Map.entry("__builtin_print", new BuiltinFunction(1, Builtins::print)),
21 Map.entry("__builtin_randInt", new BuiltinFunction(2, Builtins::randInt)),
22 Map.entry("__builtin_readLn", new BuiltinFunction(1, Builtins::readLn))
23 );
24
25 private Builtins() {}
26
27 private static Value isRepl(List<Value> args) {
28 return OrangBoolean.TRUE;
29 }
30
31 private static Value len(List<Value> args) throws OrangRuntimeException {
32 assert args.size() == 1;
33 var arg = args.getFirst();
34
35 if (arg instanceof OrangString(var value)) {
36 return new OrangInteger(value.length());
37 } else if (arg instanceof Array(var items)) {
38 return new OrangInteger(items.size());
39 } else {
40 throw new OrangRuntimeException(STR."len \{arg.typeName()} is not implemented");
41 }
42 }
43
44 private static Value parseInt(List<Value> args) throws OrangRuntimeException {
45 assert args.size() == 1;
46
47 var arg = args.getFirst();
48 if (arg instanceof OrangInteger) {
49 log.warn("Attempted to parseInt an Integer!");
50 return arg;
51 } else if (arg instanceof OrangString(var value)) {
52 try {
53 return new OrangInteger(Integer.parseInt(value.trim()));
54 } catch (NumberFormatException ex) {
55 throw new OrangRuntimeException(ex);
56 }
57 } else {
58 throw new OrangRuntimeException(STR."parseInt \{arg.typeName()} is not implemented");
59 }
60 }
61
62 private static Value print(List<Value> args) {
63 assert args.size() == 1;
64 STDOUT.print(args.getFirst().display());
65 STDOUT.flush();
66 return args.getFirst();
67 }
68
69 private static Value randInt(List<Value> args) throws OrangRuntimeException {
70 assert args.size() == 2;
71
72 var minv = args.getFirst();
73 var maxv = args.get(1);
74 if (minv instanceof OrangInteger(var min) && maxv instanceof OrangInteger(var max)) {
75 return new OrangInteger(new Random().nextInt(min, max));
76 } else {
77 throw new OrangRuntimeException(STR."randInt \{minv.typeName()} \{maxv.typeName()} is not implemented");
78 }
79 }
80
81 private static Value readLn(List<Value> args) throws OrangRuntimeException {
82 assert args.size() == 1;
83
84 var arg = args.getFirst();
85 if (!(arg instanceof Array) || !((Array) arg).items().isEmpty()) {
86 log.warn("You should call readLn with an empty tuple like `readLn ()`");
87 }
88
89 try {
90 return new OrangString(STDIN.readLine());
91 } catch (IOException e) {
92 throw new OrangRuntimeException(e);
93 }
94 }
95}
diff --git a/src/main/java/lv/enes/orang/Codepoint.java b/src/main/java/lv/enes/orang/Codepoint.java
new file mode 100644
index 0000000..0dfaa2b
--- /dev/null
+++ b/src/main/java/lv/enes/orang/Codepoint.java
@@ -0,0 +1,24 @@
1package lv.enes.orang;
2
3public record Codepoint(int cp) {
4 @Override
5 public String toString() {
6 return Character.toString(cp);
7 }
8
9 public boolean isIdentInitial() {
10 return Character.isLetter(cp) || cp == '_';
11 }
12
13 public boolean isIdentFinal() {
14 return isIdentInitial() || Character.isDigit(cp);
15 }
16
17 public boolean isNumeral() {
18 return Character.isDigit(cp);
19 }
20
21 public boolean isWhitespace() {
22 return Character.isWhitespace(cp);
23 }
24}
diff --git a/src/main/java/lv/enes/orang/Lexer.java b/src/main/java/lv/enes/orang/Lexer.java
new file mode 100644
index 0000000..0ee2503
--- /dev/null
+++ b/src/main/java/lv/enes/orang/Lexer.java
@@ -0,0 +1,168 @@
1package lv.enes.orang;
2
3import java.io.*;
4import java.util.Iterator;
5import java.util.function.BiFunction;
6import java.util.function.Predicate;
7import java.util.stream.IntStream;
8import java.util.stream.Stream;
9
10public class Lexer implements Iterator<Token> {
11 private final PeekableStream<Codepoint> input;
12
13 public Lexer(InputStream input) {
14 this(new InputStreamReader(input));
15 }
16
17 public Lexer(Reader input) {
18 var cpStream = new BufferedReader(input)
19 .lines()
20 .flatMapToInt(str -> IntStream.concat(str.codePoints(), IntStream.of('\n')))
21 .mapToObj(Codepoint::new);
22 var theEof = Stream.of(new Codepoint(-1));
23 this.input = new PeekableStream<>(Stream.concat(cpStream, theEof).iterator());
24 }
25
26 public Lexer(String input) {
27 this(new StringReader(input));
28 }
29
30 private boolean hasNext = true;
31
32 @Override
33 public Token next() {
34 var tok = nextToken();
35 if (tok.type() == TokenType.EOF) {
36 hasNext = false;
37 }
38 return tok;
39 }
40
41 @Override
42 public boolean hasNext() {
43 return hasNext;
44 }
45
46 private Token nextToken() {
47 skipWhitespace();
48 return switch (input.peek().cp()) {
49 case -1 -> new Token(TokenType.EOF, "");
50
51 case '*' -> new Token(TokenType.ASTERISK, input.next());
52 case '!' -> new Token(TokenType.BANG, input.next());
53 case '[' -> new Token(TokenType.BRACKET_LEFT, input.next());
54 case ']' -> new Token(TokenType.BRACKET_RIGHT, input.next());
55 case ',' -> new Token(TokenType.COMMA, input.next());
56 case '=' -> new Token(TokenType.EQUAL, input.next());
57 case '>' -> {
58 var first = input.next();
59 if (input.peek().cp() == '=') {
60 yield new Token(TokenType.GREATER_EQUAL, first, input.next());
61 } else {
62 yield new Token(TokenType.GREATER, first);
63 }
64 }
65 case '<' -> {
66 var first = input.next();
67 if (input.peek().cp() == '=') {
68 yield new Token(TokenType.LESS_EQUAL, first, input.next());
69 } else {
70 yield new Token(TokenType.LESS, first);
71 }
72 }
73 case '-' -> {
74 var first = input.next();
75 if (input.peek().cp() == '>') {
76 yield new Token(TokenType.MINUS_GREATER, first, input.next());
77 } else {
78 yield new Token(TokenType.MINUS, first);
79 }
80 }
81 case '(' -> new Token(TokenType.PAREN_LEFT, input.next());
82 case ')' -> new Token(TokenType.PAREN_RIGHT, input.next());
83 case '+' -> new Token(TokenType.PLUS, input.next());
84 case '?' -> {
85 var first = input.next();
86 if (input.peek().cp() == '=') {
87 yield new Token(TokenType.QUESTION_EQUAL, first, input.next());
88 } else {
89 yield new Token(TokenType.ILLEGAL, first, input.next());
90 }
91 }
92 case ';' -> new Token(TokenType.SEMICOLON, input.next());
93 case '/' -> {
94 var first = input.next();
95 if (input.peek().cp() == '=') {
96 yield new Token(TokenType.SLASH_EQUAL, first, input.next());
97 } else {
98 yield new Token(TokenType.SLASH, first);
99 }
100 }
101
102 case '"' -> new Token(TokenType.STRING, readString());
103
104 default -> {
105 if (input.peek().isIdentInitial()) {
106 var ident = readIdentifier();
107 var type = switch (ident) {
108 case "and" -> TokenType.AND;
109 case "def" -> TokenType.DEF;
110 case "do" -> TokenType.DO;
111 case "else" -> TokenType.ELSE;
112 case "end" -> TokenType.END;
113 case "false" -> TokenType.FALSE;
114 case "fn" -> TokenType.FN;
115 case "if" -> TokenType.IF;
116 case "in" -> TokenType.IN;
117 case "let" -> TokenType.LET;
118 case "then" -> TokenType.THEN;
119 case "true" -> TokenType.TRUE;
120 default -> TokenType.IDENTIFIER;
121 };
122 yield new Token(type, ident);
123 } else if (input.peek().isNumeral()) {
124 yield new Token(TokenType.INTEGER, readInteger());
125 } else {
126 yield new Token(TokenType.ILLEGAL, input.next());
127 }
128 }
129 };
130 }
131
132 private <T> T foldWhile(Predicate<Codepoint> pred, T initial, BiFunction<T, Codepoint, T> combine) {
133 var res = initial;
134 var ch = input.peek();
135 while (pred.test(ch)) {
136 res = combine.apply(res, input.next());
137 ch = input.peek();
138 }
139 return res;
140 }
141
142 private String readWhile(Predicate<Codepoint> pred) {
143 return foldWhile(pred, new StringBuilder(), StringBuilder::append).toString();
144 }
145
146 private void skipWhile(Predicate<Codepoint> pred) {
147 foldWhile(pred, Object.class, (x, _) -> x);
148 }
149
150 private String readIdentifier() {
151 return readWhile(Codepoint::isIdentFinal);
152 }
153
154 private String readInteger() {
155 return readWhile(Codepoint::isNumeral);
156 }
157
158 private String readString() {
159 input.next();
160 var literal = readWhile(cp -> cp.cp() != '"');
161 input.next();
162 return literal;
163 }
164
165 private void skipWhitespace() {
166 skipWhile(Codepoint::isWhitespace);
167 }
168}
diff --git a/src/main/java/lv/enes/orang/Main.java b/src/main/java/lv/enes/orang/Main.java
new file mode 100644
index 0000000..9a607ed
--- /dev/null
+++ b/src/main/java/lv/enes/orang/Main.java
@@ -0,0 +1,77 @@
1package lv.enes.orang;
2
3import java.io.FileReader;
4import java.io.IOException;
5
6import static lv.enes.orang.State.STDIN;
7import static lv.enes.orang.State.STDOUT;
8
9public class Main {
10 public static final String PROMPT = ">> ";
11
12 public static void main() throws IOException {
13 repl();
14 }
15
16 private static void repl() throws IOException {
17 var scope = new Scope();
18
19 try (var stream = Main.class.getResourceAsStream("prelude.orang")) {
20 var prog = Parser.parseProgram(stream);
21 scope = prog.runStatement(scope);
22 } catch (OrangException ex) {
23 STDOUT.println(STR."While evaluating prelude: \{ex}");
24 throw new RuntimeException(ex);
25 }
26
27 boolean running = true;
28 while (running) {
29 STDOUT.print(PROMPT);
30 STDOUT.flush();
31
32 var line = STDIN.readLine();
33 if (line == null) {
34 return;
35 }
36
37 if (line.isEmpty()) {
38 continue;
39 }
40
41 if (line.charAt(0) == ':') {
42 if (line.length() == 1) {
43 continue;
44 }
45
46 switch (line.charAt(1)) {
47 case 'l':
48 var filename = line.substring(2).trim();
49 try (var reader = new FileReader((filename))) {
50 var prog = Parser.parseProgram(reader);
51 scope = prog.runStatement(scope);
52 } catch (IOException | OrangException ex) {
53 STDOUT.println(ex);
54 }
55 break;
56 case 'q':
57 running = false;
58 break;
59 default:
60 STDOUT.println("Unrecognised REPL command");
61 }
62 continue;
63 }
64
65 try {
66 var prog = Parser.parseProgram(line);
67 scope = prog.runStatement(scope);
68 if (scope.getLastResult() != null) {
69 STDOUT.print("-> ");
70 STDOUT.println(scope.getLastResult().stringify());
71 }
72 } catch (OrangException ex) {
73 STDOUT.println(ex);
74 }
75 }
76 }
77}
diff --git a/src/main/java/lv/enes/orang/NonEmptyList.java b/src/main/java/lv/enes/orang/NonEmptyList.java
new file mode 100644
index 0000000..b532a18
--- /dev/null
+++ b/src/main/java/lv/enes/orang/NonEmptyList.java
@@ -0,0 +1,23 @@
1package lv.enes.orang;
2
3import java.util.AbstractList;
4import java.util.List;
5
6public class NonEmptyList<E> extends AbstractList<E> {
7 private final List<E> delegate;
8
9 public NonEmptyList(List<E> delegate) {
10 assert !delegate.isEmpty();
11 this.delegate = List.copyOf(delegate);
12 }
13
14 @Override
15 public E get(int index) {
16 return delegate.get(index);
17 }
18
19 @Override
20 public int size() {
21 return delegate.size();
22 }
23}
diff --git a/src/main/java/lv/enes/orang/OrangException.java b/src/main/java/lv/enes/orang/OrangException.java
new file mode 100644
index 0000000..9308643
--- /dev/null
+++ b/src/main/java/lv/enes/orang/OrangException.java
@@ -0,0 +1,11 @@
1package lv.enes.orang;
2
3public abstract class OrangException extends Exception {
4 protected OrangException(String message) {
5 super(message);
6 }
7
8 protected OrangException(Throwable cause) {
9 super(cause);
10 }
11}
diff --git a/src/main/java/lv/enes/orang/OrangRuntimeException.java b/src/main/java/lv/enes/orang/OrangRuntimeException.java
new file mode 100644
index 0000000..9648c30
--- /dev/null
+++ b/src/main/java/lv/enes/orang/OrangRuntimeException.java
@@ -0,0 +1,11 @@
1package lv.enes.orang;
2
3public class OrangRuntimeException extends OrangException {
4 public OrangRuntimeException(String message) {
5 super(message);
6 }
7
8 public OrangRuntimeException(Throwable cause) {
9 super(cause);
10 }
11}
diff --git a/src/main/java/lv/enes/orang/Parser.java b/src/main/java/lv/enes/orang/Parser.java
new file mode 100644
index 0000000..fd4b9bb
--- /dev/null
+++ b/src/main/java/lv/enes/orang/Parser.java
@@ -0,0 +1,291 @@
1package lv.enes.orang;
2
3import lv.enes.orang.ast.*;
4import lv.enes.orang.ast.IfElseExpression;
5import lv.enes.orang.ast.Statement;
6
7import java.io.InputStream;
8import java.io.Reader;
9import java.util.ArrayList;
10import java.util.Collections;
11import java.util.Iterator;
12import java.util.List;
13import java.util.function.Predicate;
14
15public class Parser {
16 public static Program parseProgram(InputStream in) throws ParserException {
17 var parser = new Parser(in);
18 return parser.parseProgram();
19 }
20
21 public static Program parseProgram(Reader in) throws ParserException {
22 var parser = new Parser(in);
23 return parser.parseProgram();
24 }
25
26 public static Program parseProgram(String in) throws ParserException {
27 var parser = new Parser(in);
28 return parser.parseProgram();
29 }
30
31 private final PeekableStream<Token> input;
32
33 public Parser(InputStream in) {
34 this(new Lexer(in));
35 }
36
37 public Parser(Reader in) {
38 this(new Lexer(in));
39 }
40
41 public Parser(String in) {
42 this(new Lexer(in));
43 }
44
45 public Parser(Iterator<Token> input) {
46 this.input = new PeekableStream<>(input);
47 }
48
49 public Program parseProgram() throws ParserException {
50 var statements = new ArrayList<Statement>();
51 while (!maybeConsumeToken(TokenType.EOF)) {
52 statements.add(parseStatement());
53 maybeConsumeToken(TokenType.SEMICOLON);
54 }
55 return new Program(Collections.unmodifiableList(statements));
56 }
57
58 private Token consume(Predicate<Token> pred, String msg) throws ParserException {
59 var tok = input.next();
60 if (!pred.test(tok)) {
61 throw new ParserException(STR."\{msg}, got \{tok}");
62 }
63 return tok;
64 }
65
66 private Token consumeToken(TokenType type) throws ParserException {
67 return consume(tok -> tok.type() == type, STR."Expected \{type}");
68 }
69
70 private boolean maybeConsumeToken(TokenType type) {
71 if (input.peek().type() == type) {
72 input.next();
73 return true;
74 }
75 return false;
76 }
77
78 private ArrayExpression parseArray() throws ParserException {
79 consumeToken(TokenType.BRACKET_LEFT);
80 if (maybeConsumeToken(TokenType.BRACKET_RIGHT)) {
81 return new ArrayExpression(List.of());
82 }
83
84 var items = new ArrayList<Expression>();
85 do {
86 items.add(parseExpression());
87 } while (maybeConsumeToken(TokenType.COMMA));
88 consumeToken(TokenType.BRACKET_RIGHT);
89
90 return new ArrayExpression(Collections.unmodifiableList(items));
91 }
92
93 private List<String> parseArgSpecs() {
94 var argSpecs = new ArrayList<String>();
95 while (true) {
96 if (input.peek().type() == TokenType.IDENTIFIER) {
97 argSpecs.add(input.next().literal());
98 } else {
99 break;
100 }
101 }
102 return Collections.unmodifiableList(argSpecs);
103 }
104
105 private Expression parseBinaryExpression() throws ParserException {
106 var lhs = parseCallExpression();
107 if (!input.peek().type().isBinaryOp()) {
108 return lhs;
109 }
110
111 return parseBinaryExpressionRhs(lhs, input.next().type().toBinaryOp());
112 }
113
114 private Expression parseBinaryExpressionRhs(Expression lhs, BinaryExpression.Operator op) throws ParserException {
115 var rhs = parseCallExpression();
116 if (!input.peek().type().isBinaryOp()) {
117 return new BinaryExpression(op, lhs, rhs);
118 }
119
120 var op2 = input.next().type().toBinaryOp();
121 if (op2.bindsStrongerThan(op)) {
122 return new BinaryExpression(op, lhs, parseBinaryExpressionRhs(rhs, op2));
123 } else {
124 return parseBinaryExpressionRhs(new BinaryExpression(op, lhs, rhs), op2);
125 }
126 }
127
128 private BooleanLiteral parseBoolean() throws ParserException {
129 var t = consume(tok -> tok.type() == TokenType.FALSE || tok.type() == TokenType.TRUE, "Expected TRUE or FALSE");
130 return new BooleanLiteral(t.type() == TokenType.TRUE);
131 }
132
133 private Expression parseCallExpression() throws ParserException {
134 var callee = parseSimpleExpression();
135 while (couldStartSimpleExpression(input.peek().type())) {
136 var arg = parseSimpleExpression();
137 callee = new CallExpression(callee, arg);
138 }
139 return callee;
140 }
141
142 private Definition parseDefinition() throws ParserException {
143 consumeToken(TokenType.DEF);
144 var defSpec = parseDefSpec();
145 consumeToken(TokenType.EQUAL);
146 var value = parseExpression();
147 if (defSpec.args().isEmpty()) {
148 return new Definition(defSpec.name(), value);
149 } else {
150 return new Definition(defSpec.name(), new FnExpression(new NonEmptyList<>(defSpec.args()), value));
151 }
152 }
153
154 private DefSpec parseDefSpec() throws ParserException {
155 var name = consumeToken(TokenType.IDENTIFIER).literal();
156 var argSpecs = parseArgSpecs();
157 return new DefSpec(name, argSpecs);
158 }
159
160 private DoExpression parseDoExpression() throws ParserException {
161 consumeToken(TokenType.DO);
162 var exprs = new ArrayList<Expression>();
163 do {
164 exprs.add(parseExpression());
165 } while (maybeConsumeToken(TokenType.SEMICOLON));
166 consumeToken(TokenType.END);
167 return new DoExpression(Collections.unmodifiableList(exprs));
168 }
169
170 private Expression parseExpression() throws ParserException {
171 if (input.peek().type().isUnaryOp()) {
172 return parseUnaryExpression();
173 }
174 return parseBinaryExpression();
175 }
176
177 private FnExpression parseFnExpression() throws ParserException {
178 consumeToken(TokenType.FN);
179 var argSpecs = parseArgSpecs();
180 if (argSpecs.isEmpty()) {
181 throw new ParserException("Function definition with no arguments");
182 }
183 var body = maybeConsumeToken(TokenType.MINUS_GREATER) ? parseExpression() : parseDoExpression();
184 return new FnExpression(new NonEmptyList<>(argSpecs), body);
185 }
186
187 private IfElseExpression parseIfElseExpression() throws ParserException {
188 consumeToken(TokenType.IF);
189 var cond = parseExpression();
190 consumeToken(TokenType.THEN);
191 var trueBranch = parseExpression();
192 consumeToken(TokenType.ELSE);
193 var falseBranch = parseExpression();
194 return new IfElseExpression(cond, trueBranch, falseBranch);
195 }
196
197 private IntLiteral parseInteger() throws ParserException {
198 var tok = consumeToken(TokenType.INTEGER);
199 return new IntLiteral(Integer.parseInt(tok.literal()));
200 }
201
202 private LetInExpression parseLetInExpression() throws ParserException {
203 consumeToken(TokenType.LET);
204 var bindings = new ArrayList<LetInExpression.Binding>();
205 do {
206 var defSpec = parseDefSpec();
207 consumeToken(TokenType.EQUAL);
208 var value = parseExpression();
209 if (defSpec.args().isEmpty()) {
210 bindings.add(new LetInExpression.Binding(defSpec.name(), value));
211 } else {
212 var fn = new FnExpression(new NonEmptyList<>(defSpec.args()), value);
213 bindings.add(new LetInExpression.Binding(defSpec.name(), fn));
214 }
215 } while (maybeConsumeToken(TokenType.AND));
216 consumeToken(TokenType.IN);
217 var body = parseExpression();
218 return new LetInExpression(Collections.unmodifiableList(bindings), body);
219 }
220
221 private Expression parseSimpleExpression() throws ParserException {
222 return switch (input.peek().type()) {
223 case PAREN_LEFT -> {
224 consumeToken(TokenType.PAREN_LEFT);
225 if (maybeConsumeToken(TokenType.PAREN_RIGHT)) {
226 yield new ArrayExpression(List.of());
227 }
228 var expr = parseExpression();
229 consumeToken(TokenType.PAREN_RIGHT);
230 yield expr;
231 }
232 case TRUE, FALSE -> parseBoolean();
233 case INTEGER -> parseInteger();
234 case IDENTIFIER -> new VariableExpression(input.next().literal());
235 case STRING -> parseString();
236 case BRACKET_LEFT -> parseArray();
237 case IF -> parseIfElseExpression();
238 case LET -> parseLetInExpression();
239 case FN -> parseFnExpression();
240 case DO -> parseDoExpression();
241 default -> throw new ParserException(STR."Unexpected token \{input.peek()}");
242 };
243 }
244
245 private boolean couldStartSimpleExpression(TokenType type) {
246 return switch (type) {
247 case PAREN_LEFT, TRUE, FALSE, INTEGER, IDENTIFIER, STRING, BRACKET_LEFT, IF, LET, FN, DO -> true;
248 default -> false;
249 };
250 }
251
252 private Statement parseStatement() throws ParserException {
253 if (input.peek().type() == TokenType.DEF) {
254 return parseDefinition();
255 } else {
256 return parseExpression();
257 }
258 }
259
260 private Expression parseString() throws ParserException {
261 var sb = new StringBuilder();
262 var cps = input.next().literal().codePoints().iterator();
263 while (cps.hasNext()) {
264 var cp = cps.next();
265 if (cp == '\\') {
266 var escapeChar = cps.next();
267 //noinspection UnnecessaryUnboxing
268 sb.append(switch (escapeChar.intValue()) {
269 case '\'' -> '\'';
270 case '"' -> '"';
271 case 'r' -> '\r';
272 case 'n' -> '\n';
273 case 't' -> '\t';
274 default -> throw new ParserException(STR."Unknown string escape '\\\{escapeChar}'");
275 });
276 } else {
277 sb.appendCodePoint(cp);
278 }
279 }
280 return new StringLiteral(sb.toString());
281 }
282
283 private Expression parseUnaryExpression() throws ParserException {
284 if (input.peek().type().isUnaryOp()) {
285 var op = input.next().type().toUnaryOp();
286 return new UnaryExpression(op, parseUnaryExpression());
287 } else {
288 return parseSimpleExpression();
289 }
290 }
291}
diff --git a/src/main/java/lv/enes/orang/ParserException.java b/src/main/java/lv/enes/orang/ParserException.java
new file mode 100644
index 0000000..9000a93
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ParserException.java
@@ -0,0 +1,7 @@
1package lv.enes.orang;
2
3public class ParserException extends OrangException {
4 public ParserException(String message) {
5 super(message);
6 }
7}
diff --git a/src/main/java/lv/enes/orang/PeekableStream.java b/src/main/java/lv/enes/orang/PeekableStream.java
new file mode 100644
index 0000000..b77bab1
--- /dev/null
+++ b/src/main/java/lv/enes/orang/PeekableStream.java
@@ -0,0 +1,38 @@
1package lv.enes.orang;
2
3import java.util.ArrayDeque;
4import java.util.Deque;
5import java.util.Iterator;
6
7public class PeekableStream<T> implements Iterator<T> {
8 private final Iterator<T> input;
9 private final Deque<T> buffer = new ArrayDeque<>();
10
11 public PeekableStream(Iterator<T> input) {
12 this.input = input;
13 }
14
15 @Override
16 public boolean hasNext() {
17 return !buffer.isEmpty() || input.hasNext();
18 }
19
20 @Override
21 public T next() {
22 if (!buffer.isEmpty()) {
23 return buffer.pop();
24 } else {
25 return input.next();
26 }
27 }
28
29 public T peek() {
30 var value = next();
31 putBack(value);
32 return value;
33 }
34
35 public void putBack(T value) {
36 buffer.push(value);
37 }
38}
diff --git a/src/main/java/lv/enes/orang/Scope.java b/src/main/java/lv/enes/orang/Scope.java
new file mode 100644
index 0000000..3625c69
--- /dev/null
+++ b/src/main/java/lv/enes/orang/Scope.java
@@ -0,0 +1,54 @@
1package lv.enes.orang;
2
3import jakarta.annotation.Nullable;
4import lv.enes.orang.value.Value;
5
6import java.util.HashMap;
7import java.util.Map;
8
9public class Scope {
10 private final Map<String, Value> definitions;
11
12 private final Value lastResult;
13
14 public Scope() {
15 this(new HashMap<>(Builtins.BUILTINS), null);
16 }
17
18 private Scope(Map<String, Value> definitions, Value lastResult) {
19 this.definitions = definitions;
20 this.lastResult = lastResult;
21 }
22
23 public Value getDefinition(String name) throws OrangRuntimeException {
24 if (definitions.containsKey(name)) {
25 return definitions.get(name);
26 }
27 throw new OrangRuntimeException(STR."Value named \{name} is not defined!");
28 }
29
30 @Nullable
31 public Value getLastResult() {
32 return lastResult;
33 }
34
35 public Scope withDefinition(String key, Value value) {
36 var newDefs = new HashMap<>(definitions);
37 newDefs.put(key, value);
38 return new Scope(newDefs, null);
39 }
40
41 public Scope withDefinitions(Map<String, Value> definitions) {
42 var newDefs = new HashMap<>(this.definitions);
43 newDefs.putAll(definitions);
44 return new Scope(newDefs, null);
45 }
46
47 public void setDefinition(String key, Value value) {
48 definitions.put(key, value);
49 }
50
51 public Scope withLastResult(Value value) {
52 return new Scope(definitions, value);
53 }
54}
diff --git a/src/main/java/lv/enes/orang/State.java b/src/main/java/lv/enes/orang/State.java
new file mode 100644
index 0000000..46b115b
--- /dev/null
+++ b/src/main/java/lv/enes/orang/State.java
@@ -0,0 +1,12 @@
1package lv.enes.orang;
2
3import java.io.BufferedReader;
4import java.io.InputStreamReader;
5import java.io.PrintWriter;
6
7public final class State {
8 public static final BufferedReader STDIN = new BufferedReader(new InputStreamReader(System.in));
9 public static final PrintWriter STDOUT = new PrintWriter(System.out);
10
11 private State() {}
12}
diff --git a/src/main/java/lv/enes/orang/Token.java b/src/main/java/lv/enes/orang/Token.java
new file mode 100644
index 0000000..4456b8f
--- /dev/null
+++ b/src/main/java/lv/enes/orang/Token.java
@@ -0,0 +1,15 @@
1package lv.enes.orang;
2
3public record Token(TokenType type, String literal) {
4 public Token(TokenType type, Codepoint... cps) {
5 this(type, codepointsToString(cps));
6 }
7
8 private static String codepointsToString(Codepoint... cps) {
9 var sb = new StringBuilder(cps.length);
10 for (var cp : cps) {
11 sb.append(cp);
12 }
13 return sb.toString();
14 }
15}
diff --git a/src/main/java/lv/enes/orang/TokenType.java b/src/main/java/lv/enes/orang/TokenType.java
new file mode 100644
index 0000000..960435e
--- /dev/null
+++ b/src/main/java/lv/enes/orang/TokenType.java
@@ -0,0 +1,91 @@
1package lv.enes.orang;
2
3import lv.enes.orang.ast.BinaryExpression;
4import lv.enes.orang.ast.UnaryExpression;
5
6public enum TokenType {
7 ILLEGAL,
8 EOF,
9
10 // Literals
11 IDENTIFIER,
12 INTEGER,
13 STRING,
14
15 // Keywords
16 AND,
17 DEF,
18 DO,
19 ELSE,
20 END,
21 FALSE,
22 FN,
23 IF,
24 IN,
25 LET,
26 THEN,
27 TRUE,
28
29 // Special chars
30 ASTERISK,
31 BANG,
32 BRACKET_LEFT,
33 BRACKET_RIGHT,
34 COMMA,
35 EQUAL,
36 GREATER,
37 GREATER_EQUAL,
38 LESS,
39 LESS_EQUAL,
40 MINUS,
41 MINUS_GREATER,
42 PAREN_LEFT,
43 PAREN_RIGHT,
44 PLUS,
45 QUESTION_EQUAL,
46 SEMICOLON,
47 SLASH,
48 SLASH_EQUAL,
49
50 ;
51
52 public boolean isBinaryOp() {
53 return switch (this) {
54 case ASTERISK, SLASH, PLUS, MINUS, QUESTION_EQUAL, SLASH_EQUAL, GREATER, GREATER_EQUAL, LESS, LESS_EQUAL
55 -> true;
56 default -> false;
57 };
58 }
59
60 public BinaryExpression.Operator toBinaryOp() {
61 return switch (this) {
62 case ASTERISK -> BinaryExpression.Operator.MULTIPLY;
63 case SLASH -> BinaryExpression.Operator.DIVIDE;
64 case PLUS -> BinaryExpression.Operator.ADD;
65 case MINUS -> BinaryExpression.Operator.SUBTRACT;
66 case QUESTION_EQUAL -> BinaryExpression.Operator.EQUALS;
67 case SLASH_EQUAL -> BinaryExpression.Operator.NOT_EQUALS;
68 case GREATER -> BinaryExpression.Operator.GT;
69 case GREATER_EQUAL -> BinaryExpression.Operator.GTE;
70 case LESS -> BinaryExpression.Operator.LT;
71 case LESS_EQUAL -> BinaryExpression.Operator.LTE;
72 default -> throw new IllegalStateException("Token " + this + " is not a binary operator");
73 };
74 }
75
76 public boolean isUnaryOp() {
77 return switch (this) {
78 case PLUS, MINUS, BANG -> true;
79 default -> false;
80 };
81 }
82
83 public UnaryExpression.Operator toUnaryOp() {
84 return switch (this) {
85 case PLUS -> UnaryExpression.Operator.PLUS;
86 case MINUS -> UnaryExpression.Operator.NEGATE;
87 case BANG -> UnaryExpression.Operator.NOT;
88 default -> throw new IllegalStateException("Token " + this + " is not a unary operator");
89 };
90 }
91}
diff --git a/src/main/java/lv/enes/orang/ast/ArrayExpression.java b/src/main/java/lv/enes/orang/ast/ArrayExpression.java
new file mode 100644
index 0000000..d2437cf
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ast/ArrayExpression.java
@@ -0,0 +1,21 @@
1package lv.enes.orang.ast;
2
3import lv.enes.orang.OrangRuntimeException;
4import lv.enes.orang.Scope;
5import lv.enes.orang.value.Array;
6import lv.enes.orang.value.Value;
7
8import java.util.ArrayList;
9import java.util.Collections;
10import java.util.List;
11
12public record ArrayExpression(List<Expression> items) implements Expression {
13 @Override
14 public Value evaluateExpression(Scope scope) throws OrangRuntimeException {
15 var values = new ArrayList<Value>();
16 for (var item : items) {
17 values.add(item.evaluateExpression(scope));
18 }
19 return new Array(Collections.unmodifiableList(values));
20 }
21}
diff --git a/src/main/java/lv/enes/orang/ast/BinaryExpression.java b/src/main/java/lv/enes/orang/ast/BinaryExpression.java
new file mode 100644
index 0000000..c0b988c
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ast/BinaryExpression.java
@@ -0,0 +1,53 @@
1package lv.enes.orang.ast;
2
3import lv.enes.orang.OrangRuntimeException;
4import lv.enes.orang.Scope;
5import lv.enes.orang.value.Value;
6
7public record BinaryExpression(Operator operator, Expression lhs, Expression rhs) implements Expression {
8 public enum Operator {
9 EQUALS,
10 NOT_EQUALS,
11 GT,
12 GTE,
13 LT,
14 LTE,
15 ADD,
16 SUBTRACT,
17 MULTIPLY,
18 DIVIDE,
19 ;
20
21 public boolean bindsStrongerThan(Operator other) {
22 return switch (this) {
23 case EQUALS, NOT_EQUALS, GT, GTE, LT, LTE -> false;
24 case ADD, SUBTRACT -> switch (other) {
25 case EQUALS, NOT_EQUALS, GT, GTE, LTE -> true;
26 default -> false;
27 };
28 case MULTIPLY, DIVIDE -> switch (other) {
29 case EQUALS, NOT_EQUALS, GT, GTE, LT, LTE, ADD, SUBTRACT -> true;
30 default -> false;
31 };
32 };
33 }
34 }
35
36 @Override
37 public Value evaluateExpression(Scope scope) throws OrangRuntimeException {
38 var lhs = lhs().evaluateExpression(scope);
39 var rhs = rhs().evaluateExpression(scope);
40 return switch (operator) {
41 case EQUALS -> lhs.orangEquals(rhs);
42 case NOT_EQUALS -> lhs.notEquals(rhs);
43 case GT -> lhs.greaterThan(rhs);
44 case GTE -> lhs.greaterThanOrEqual(rhs);
45 case LT -> lhs.lessThan(rhs);
46 case LTE -> lhs.lessThanOrEqual(rhs);
47 case ADD -> lhs.add(rhs);
48 case SUBTRACT -> lhs.subtract(rhs);
49 case MULTIPLY -> lhs.multiply(rhs);
50 case DIVIDE -> lhs.divide(rhs);
51 };
52 }
53}
diff --git a/src/main/java/lv/enes/orang/ast/BooleanLiteral.java b/src/main/java/lv/enes/orang/ast/BooleanLiteral.java
new file mode 100644
index 0000000..9110f67
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ast/BooleanLiteral.java
@@ -0,0 +1,12 @@
1package lv.enes.orang.ast;
2
3import lv.enes.orang.Scope;
4import lv.enes.orang.value.OrangBoolean;
5import lv.enes.orang.value.Value;
6
7public record BooleanLiteral(boolean value) implements Expression {
8 @Override
9 public Value evaluateExpression(Scope scope) {
10 return OrangBoolean.of(value);
11 }
12}
diff --git a/src/main/java/lv/enes/orang/ast/CallExpression.java b/src/main/java/lv/enes/orang/ast/CallExpression.java
new file mode 100644
index 0000000..8f05496
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ast/CallExpression.java
@@ -0,0 +1,12 @@
1package lv.enes.orang.ast;
2
3import lv.enes.orang.OrangRuntimeException;
4import lv.enes.orang.Scope;
5import lv.enes.orang.value.Value;
6
7public record CallExpression(Expression callee, Expression arg) implements Expression {
8 @Override
9 public Value evaluateExpression(Scope scope) throws OrangRuntimeException {
10 return callee.evaluateExpression(scope).call(arg.evaluateExpression(scope));
11 }
12}
diff --git a/src/main/java/lv/enes/orang/ast/DefSpec.java b/src/main/java/lv/enes/orang/ast/DefSpec.java
new file mode 100644
index 0000000..2233d3f
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ast/DefSpec.java
@@ -0,0 +1,6 @@
1package lv.enes.orang.ast;
2
3import java.util.List;
4
5public record DefSpec(String name, List<String> args) {
6}
diff --git a/src/main/java/lv/enes/orang/ast/Definition.java b/src/main/java/lv/enes/orang/ast/Definition.java
new file mode 100644
index 0000000..715daae
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ast/Definition.java
@@ -0,0 +1,15 @@
1package lv.enes.orang.ast;
2
3import lv.enes.orang.OrangRuntimeException;
4import lv.enes.orang.Scope;
5import lv.enes.orang.value.Undefined;
6
7public record Definition(String name, Expression body) implements Statement {
8 @Override
9 public Scope runStatement(Scope scope) throws OrangRuntimeException {
10 scope.setDefinition(name, Undefined.INSTANCE);
11 var value = body.evaluateExpression(scope);
12 scope.setDefinition(name, value);
13 return scope;
14 }
15}
diff --git a/src/main/java/lv/enes/orang/ast/DoExpression.java b/src/main/java/lv/enes/orang/ast/DoExpression.java
new file mode 100644
index 0000000..9d69a3e
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ast/DoExpression.java
@@ -0,0 +1,19 @@
1package lv.enes.orang.ast;
2
3import lv.enes.orang.OrangRuntimeException;
4import lv.enes.orang.Scope;
5import lv.enes.orang.value.Value;
6
7import java.util.List;
8
9public record DoExpression(List<Expression> body) implements Expression {
10 // assert body.!isEmpty()
11
12 @Override
13 public Value evaluateExpression(Scope scope) throws OrangRuntimeException {
14 for (var i = 0; i < body.size() - 1; i++) {
15 body.get(i).evaluateExpression(scope);
16 }
17 return body.getLast().evaluateExpression(scope);
18 }
19}
diff --git a/src/main/java/lv/enes/orang/ast/Expression.java b/src/main/java/lv/enes/orang/ast/Expression.java
new file mode 100644
index 0000000..a0fc3af
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ast/Expression.java
@@ -0,0 +1,14 @@
1package lv.enes.orang.ast;
2
3import lv.enes.orang.OrangRuntimeException;
4import lv.enes.orang.Scope;
5import lv.enes.orang.value.Value;
6
7public interface Expression extends Statement {
8 Value evaluateExpression(Scope scope) throws OrangRuntimeException;
9
10 @Override
11 default Scope runStatement(Scope scope) throws OrangRuntimeException {
12 return scope.withLastResult(evaluateExpression(scope));
13 }
14}
diff --git a/src/main/java/lv/enes/orang/ast/FnExpression.java b/src/main/java/lv/enes/orang/ast/FnExpression.java
new file mode 100644
index 0000000..3c3522f
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ast/FnExpression.java
@@ -0,0 +1,14 @@
1package lv.enes.orang.ast;
2
3import lv.enes.orang.NonEmptyList;
4import lv.enes.orang.OrangRuntimeException;
5import lv.enes.orang.Scope;
6import lv.enes.orang.value.Function;
7import lv.enes.orang.value.Value;
8
9public record FnExpression(NonEmptyList<String> args, Expression body) implements Expression {
10 @Override
11 public Value evaluateExpression(Scope scope) throws OrangRuntimeException {
12 return new Function(scope, args, body);
13 }
14}
diff --git a/src/main/java/lv/enes/orang/ast/IfElseExpression.java b/src/main/java/lv/enes/orang/ast/IfElseExpression.java
new file mode 100644
index 0000000..9b52cc5
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ast/IfElseExpression.java
@@ -0,0 +1,22 @@
1package lv.enes.orang.ast;
2
3import lv.enes.orang.OrangRuntimeException;
4import lv.enes.orang.Scope;
5import lv.enes.orang.value.OrangBoolean;
6import lv.enes.orang.value.Value;
7
8public record IfElseExpression(Expression condition, Expression trueBranch, Expression falseBranch) implements Expression {
9 @Override
10 public Value evaluateExpression(Scope scope) throws OrangRuntimeException {
11 var condValue = condition.evaluateExpression(scope);
12 if (condValue instanceof OrangBoolean value) {
13 if (value.value()) {
14 return trueBranch.evaluateExpression(scope);
15 } else {
16 return falseBranch.evaluateExpression(scope);
17 }
18 } else {
19 throw new OrangRuntimeException(STR."Condition in an if should be a Boolean not a \{condValue.typeName()}");
20 }
21 }
22}
diff --git a/src/main/java/lv/enes/orang/ast/IntLiteral.java b/src/main/java/lv/enes/orang/ast/IntLiteral.java
new file mode 100644
index 0000000..339ba3b
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ast/IntLiteral.java
@@ -0,0 +1,12 @@
1package lv.enes.orang.ast;
2
3import lv.enes.orang.Scope;
4import lv.enes.orang.value.OrangInteger;
5import lv.enes.orang.value.Value;
6
7public record IntLiteral(int value) implements Expression {
8 @Override
9 public Value evaluateExpression(Scope scope) {
10 return new OrangInteger(value);
11 }
12}
diff --git a/src/main/java/lv/enes/orang/ast/LetInExpression.java b/src/main/java/lv/enes/orang/ast/LetInExpression.java
new file mode 100644
index 0000000..2379902
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ast/LetInExpression.java
@@ -0,0 +1,27 @@
1package lv.enes.orang.ast;
2
3import lv.enes.orang.OrangRuntimeException;
4import lv.enes.orang.Scope;
5import lv.enes.orang.value.Undefined;
6import lv.enes.orang.value.Value;
7
8import java.util.HashMap;
9import java.util.List;
10
11public record LetInExpression(List<Binding> bindings, Expression body) implements Expression {
12 @Override
13 public Value evaluateExpression(Scope scope) throws OrangRuntimeException {
14 var newDefs = new HashMap<String, Value>();
15 for (var binding : bindings) {
16 newDefs.put(binding.name, Undefined.INSTANCE);
17 }
18 var newState = scope.withDefinitions(newDefs);
19 for (var binding : bindings) {
20 var value = binding.value.evaluateExpression(newState);
21 newState.setDefinition(binding.name, value);
22 }
23 return body.evaluateExpression(newState);
24 }
25
26 public record Binding(String name, Expression value) {}
27}
diff --git a/src/main/java/lv/enes/orang/ast/Program.java b/src/main/java/lv/enes/orang/ast/Program.java
new file mode 100644
index 0000000..ae5d31e
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ast/Program.java
@@ -0,0 +1,17 @@
1package lv.enes.orang.ast;
2
3import lv.enes.orang.OrangRuntimeException;
4import lv.enes.orang.Scope;
5
6import java.util.List;
7
8public record Program(List<Statement> statements) implements Statement {
9 @Override
10 public Scope runStatement(Scope inScope) throws OrangRuntimeException {
11 var state = inScope;
12 for (var statement : statements) {
13 state = statement.runStatement(state);
14 }
15 return state;
16 }
17}
diff --git a/src/main/java/lv/enes/orang/ast/Statement.java b/src/main/java/lv/enes/orang/ast/Statement.java
new file mode 100644
index 0000000..62b76bd
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ast/Statement.java
@@ -0,0 +1,8 @@
1package lv.enes.orang.ast;
2
3import lv.enes.orang.OrangRuntimeException;
4import lv.enes.orang.Scope;
5
6public interface Statement {
7 Scope runStatement(Scope scope) throws OrangRuntimeException, OrangRuntimeException;
8}
diff --git a/src/main/java/lv/enes/orang/ast/StringLiteral.java b/src/main/java/lv/enes/orang/ast/StringLiteral.java
new file mode 100644
index 0000000..367dff5
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ast/StringLiteral.java
@@ -0,0 +1,13 @@
1package lv.enes.orang.ast;
2
3import lv.enes.orang.OrangRuntimeException;
4import lv.enes.orang.Scope;
5import lv.enes.orang.value.OrangString;
6import lv.enes.orang.value.Value;
7
8public record StringLiteral(String value) implements Expression {
9 @Override
10 public Value evaluateExpression(Scope scope) throws OrangRuntimeException {
11 return new OrangString(value);
12 }
13}
diff --git a/src/main/java/lv/enes/orang/ast/UnaryExpression.java b/src/main/java/lv/enes/orang/ast/UnaryExpression.java
new file mode 100644
index 0000000..a0f565b
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ast/UnaryExpression.java
@@ -0,0 +1,23 @@
1package lv.enes.orang.ast;
2
3import lv.enes.orang.OrangRuntimeException;
4import lv.enes.orang.Scope;
5import lv.enes.orang.value.Value;
6
7public record UnaryExpression(Operator operator, Expression child) implements Expression {
8 public enum Operator {
9 PLUS,
10 NEGATE,
11 NOT,
12 }
13
14 @Override
15 public Value evaluateExpression(Scope scope) throws OrangRuntimeException {
16 var child = child().evaluateExpression(scope);
17 return switch (operator) {
18 case PLUS -> child.plus();
19 case NEGATE -> child.negate();
20 case NOT -> child.not();
21 };
22 }
23}
diff --git a/src/main/java/lv/enes/orang/ast/VariableExpression.java b/src/main/java/lv/enes/orang/ast/VariableExpression.java
new file mode 100644
index 0000000..1f5334d
--- /dev/null
+++ b/src/main/java/lv/enes/orang/ast/VariableExpression.java
@@ -0,0 +1,12 @@
1package lv.enes.orang.ast;
2
3import lv.enes.orang.OrangRuntimeException;
4import lv.enes.orang.Scope;
5import lv.enes.orang.value.Value;
6
7public record VariableExpression(String name) implements Expression {
8 @Override
9 public Value evaluateExpression(Scope scope) throws OrangRuntimeException {
10 return scope.getDefinition(name);
11 }
12}
diff --git a/src/main/java/lv/enes/orang/value/Array.java b/src/main/java/lv/enes/orang/value/Array.java
new file mode 100644
index 0000000..c3b645d
--- /dev/null
+++ b/src/main/java/lv/enes/orang/value/Array.java
@@ -0,0 +1,65 @@
1package lv.enes.orang.value;
2
3import lv.enes.orang.OrangRuntimeException;
4
5import java.util.ArrayList;
6import java.util.Collections;
7import java.util.List;
8
9public record Array(List<Value> items) implements Value {
10 @Override
11 public String typeName() {
12 return "Array";
13 }
14
15 @Override
16 public String stringify() {
17 if (items.isEmpty()) {
18 return "[]";
19 }
20 var sb = new StringBuilder("[");
21 sb.append(items.getFirst().stringify());
22 for (int i = 1; i < items.size(); i++) {
23 sb.append(", ").append(items.get(i).stringify());
24 }
25 sb.append("]");
26 return sb.toString();
27 }
28
29 @Override
30 public String display() {
31 if (items.isEmpty()) {
32 return "";
33 }
34 var sb = new StringBuilder();
35 sb.append(items.getFirst().display());
36 for (var i = 1; i < items.size(); i++) {
37 sb.append(" ").append(items.get(i).display());
38 }
39 return sb.toString();
40 }
41
42 @Override
43 public Value add(Value rhs) throws OrangRuntimeException {
44 if (rhs instanceof Array(var rhsi)) {
45 var newItems = new ArrayList<>(this.items);
46 newItems.addAll(rhsi);
47 return new Array(Collections.unmodifiableList(newItems));
48 } else {
49 throw new OrangRuntimeException(STR."add not implemented for Array and \{rhs.typeName()}");
50 }
51 }
52
53 @Override
54 public Value multiply(Value rhs) throws OrangRuntimeException {
55 if (rhs instanceof OrangInteger(var repeat)) {
56 var newItems = new ArrayList<Value>(items.size() * repeat);
57 for (var i = 0; i < repeat; i++) {
58 newItems.addAll(items);
59 }
60 return new Array(Collections.unmodifiableList(newItems));
61 } else {
62 throw new OrangRuntimeException(STR."multiply not implemented for Array and \{rhs.typeName()}");
63 }
64 }
65}
diff --git a/src/main/java/lv/enes/orang/value/BuiltinFunction.java b/src/main/java/lv/enes/orang/value/BuiltinFunction.java
new file mode 100644
index 0000000..92ccc2e
--- /dev/null
+++ b/src/main/java/lv/enes/orang/value/BuiltinFunction.java
@@ -0,0 +1,31 @@
1package lv.enes.orang.value;
2
3import lv.enes.orang.OrangRuntimeException;
4
5import java.util.List;
6
7public record BuiltinFunction(int argCount, Impl impl) implements Value {
8 @FunctionalInterface
9 public interface Impl {
10 Value apply(List<Value> args) throws OrangRuntimeException;
11 }
12
13 @Override
14 public String typeName() {
15 return "BuiltinFunction";
16 }
17
18 @Override
19 public String stringify() {
20 return "#builtinFunction";
21 }
22
23 @Override
24 public Value call(Value param) throws OrangRuntimeException {
25 if (argCount == 1) {
26 return impl.apply(List.of(param));
27 } else {
28 return new PartialBuiltinFunction(argCount, List.of(param), impl);
29 }
30 }
31}
diff --git a/src/main/java/lv/enes/orang/value/Function.java b/src/main/java/lv/enes/orang/value/Function.java
new file mode 100644
index 0000000..1f288c6
--- /dev/null
+++ b/src/main/java/lv/enes/orang/value/Function.java
@@ -0,0 +1,28 @@
1package lv.enes.orang.value;
2
3import lv.enes.orang.OrangRuntimeException;
4import lv.enes.orang.Scope;
5import lv.enes.orang.ast.Expression;
6
7import java.util.List;
8
9public record Function(Scope scope, List<String> args, Expression body) implements Value {
10 @Override
11 public String typeName() {
12 return "Function";
13 }
14
15 @Override
16 public String stringify() {
17 return "#function";
18 }
19
20 @Override
21 public Value call(Value param) throws OrangRuntimeException {
22 if (args.size() == 1) {
23 return body.evaluateExpression(scope.withDefinition(args.getFirst(), param));
24 } else {
25 return new PartialFunction(scope, args, List.of(param), body);
26 }
27 }
28}
diff --git a/src/main/java/lv/enes/orang/value/OrangBoolean.java b/src/main/java/lv/enes/orang/value/OrangBoolean.java
new file mode 100644
index 0000000..8a4776c
--- /dev/null
+++ b/src/main/java/lv/enes/orang/value/OrangBoolean.java
@@ -0,0 +1,55 @@
1package lv.enes.orang.value;
2
3import lombok.EqualsAndHashCode;
4import lv.enes.orang.OrangRuntimeException;
5
6@EqualsAndHashCode
7public final class OrangBoolean implements Value {
8 public final static OrangBoolean TRUE = new OrangBoolean(true);
9 public final static OrangBoolean FALSE = new OrangBoolean(false);
10
11 private final boolean value;
12
13 private OrangBoolean(boolean value) {
14 this.value = value;
15 }
16
17 public static OrangBoolean of(boolean value) {
18 if (value) {
19 return TRUE;
20 }
21 return FALSE;
22 }
23
24 public boolean value() {
25 return value;
26 }
27
28 @Override
29 public String typeName() {
30 return "Boolean";
31 }
32
33 @Override
34 public String stringify() {
35 if (value) {
36 return "true";
37 } else {
38 return "false";
39 }
40 }
41
42 @Override
43 public OrangBoolean not() {
44 return new OrangBoolean(!value);
45 }
46
47 @Override
48 public OrangBoolean or(Value rhs) throws OrangRuntimeException {
49 if (rhs instanceof OrangBoolean rhsb) {
50 return new OrangBoolean(value || rhsb.value);
51 } else {
52 throw new OrangRuntimeException(STR."or is not implemented for Boolean and \{rhs.typeName()}");
53 }
54 }
55}
diff --git a/src/main/java/lv/enes/orang/value/OrangInteger.java b/src/main/java/lv/enes/orang/value/OrangInteger.java
new file mode 100644
index 0000000..9b8d505
--- /dev/null
+++ b/src/main/java/lv/enes/orang/value/OrangInteger.java
@@ -0,0 +1,70 @@
1package lv.enes.orang.value;
2
3import lv.enes.orang.OrangRuntimeException;
4
5public record OrangInteger(int value) implements Value {
6 @Override
7 public String typeName() {
8 return "Integer";
9 }
10
11 @Override
12 public String stringify() {
13 return Integer.toString(value);
14 }
15
16 @Override
17 public OrangInteger negate() {
18 return new OrangInteger(-value);
19 }
20
21 @Override
22 public OrangInteger plus() {
23 return this;
24 }
25
26 @Override
27 public OrangInteger add(Value rhs) throws OrangRuntimeException {
28 if (rhs instanceof OrangInteger(int rhsi)) {
29 return new OrangInteger(value + rhsi);
30 } else {
31 throw new OrangRuntimeException(STR."add is not implemented for Integer and \{rhs.typeName()}");
32 }
33 }
34
35 @Override
36 public OrangInteger divide(Value rhs) throws OrangRuntimeException {
37 if (rhs instanceof OrangInteger(int rhsi)) {
38 return new OrangInteger(value / rhsi);
39 } else {
40 throw new OrangRuntimeException(STR."divide is not implemented for Integer and \{rhs.typeName()}");
41 }
42 }
43
44 @Override
45 public OrangInteger multiply(Value rhs) throws OrangRuntimeException {
46 if (rhs instanceof OrangInteger(int rhsi)) {
47 return new OrangInteger(value * rhsi);
48 } else {
49 throw new OrangRuntimeException(STR."multiply is not implemented for Integer and \{rhs.typeName()}");
50 }
51 }
52
53 @Override
54 public OrangInteger subtract(Value rhs) throws OrangRuntimeException {
55 if (rhs instanceof OrangInteger(int rhsi)) {
56 return new OrangInteger(value - rhsi);
57 } else {
58 throw new OrangRuntimeException(STR."subtract is not implemented for Integer and \{rhs.typeName()}");
59 }
60 }
61
62 @Override
63 public OrangBoolean greaterThan(Value rhs) throws OrangRuntimeException {
64 if (rhs instanceof OrangInteger(int rhsi)) {
65 return OrangBoolean.of(value > rhsi);
66 } else {
67 throw new OrangRuntimeException(STR."greaterThan is not implemented for Integer and \{rhs.typeName()}");
68 }
69 }
70}
diff --git a/src/main/java/lv/enes/orang/value/OrangString.java b/src/main/java/lv/enes/orang/value/OrangString.java
new file mode 100644
index 0000000..c03f7ac
--- /dev/null
+++ b/src/main/java/lv/enes/orang/value/OrangString.java
@@ -0,0 +1,49 @@
1package lv.enes.orang.value;
2
3import lv.enes.orang.OrangRuntimeException;
4
5public record OrangString(String value) implements Value {
6 @Override
7 public String typeName() {
8 return "String";
9 }
10
11 @Override
12 public String stringify() {
13 var sb = new StringBuilder("\"");
14 var cps = value.codePoints().iterator();
15 while (cps.hasNext()) {
16 var cp = cps.next();
17 if (cp == '"') {
18 sb.append("\\\"");
19 } else {
20 sb.appendCodePoint(cp);
21 }
22 }
23 sb.append('"');
24 return sb.toString();
25 }
26
27 @Override
28 public String display() {
29 return value;
30 }
31
32 @Override
33 public OrangString add(Value rhs) throws OrangRuntimeException {
34 if (rhs instanceof OrangString(String rhss)) {
35 return new OrangString(value + rhss);
36 } else {
37 throw new OrangRuntimeException(STR."add is not implemented for Integer and \{rhs.typeName()}");
38 }
39 }
40
41 @Override
42 public Value multiply(Value rhs) throws OrangRuntimeException {
43 if (rhs instanceof OrangInteger(var repeat)) {
44 return new OrangString(value.repeat(Math.max(0, repeat)));
45 } else {
46 throw new OrangRuntimeException(STR."multiply not implemented for Array and \{rhs.typeName()}");
47 }
48 }
49}
diff --git a/src/main/java/lv/enes/orang/value/PartialBuiltinFunction.java b/src/main/java/lv/enes/orang/value/PartialBuiltinFunction.java
new file mode 100644
index 0000000..37278a4
--- /dev/null
+++ b/src/main/java/lv/enes/orang/value/PartialBuiltinFunction.java
@@ -0,0 +1,32 @@
1package lv.enes.orang.value;
2
3import lv.enes.orang.OrangRuntimeException;
4
5import java.util.ArrayList;
6import java.util.Collections;
7import java.util.List;
8
9public record PartialBuiltinFunction(int argCount, List<Value> params, BuiltinFunction.Impl impl) implements Value {
10 @Override
11 public String typeName() {
12 return "BuiltinFunction";
13 }
14
15 @Override
16 public String stringify() {
17 return "#builtinFunction";
18 }
19
20 @Override
21 public Value call(Value param) throws OrangRuntimeException {
22 List<Value> newParams = new ArrayList<>(params);
23 newParams.add(param);
24 newParams = Collections.unmodifiableList(newParams);
25
26 if (newParams.size() == argCount) {
27 return impl.apply(newParams);
28 } else {
29 return new PartialBuiltinFunction(argCount, newParams, impl);
30 }
31 }
32}
diff --git a/src/main/java/lv/enes/orang/value/PartialFunction.java b/src/main/java/lv/enes/orang/value/PartialFunction.java
new file mode 100644
index 0000000..59e1466
--- /dev/null
+++ b/src/main/java/lv/enes/orang/value/PartialFunction.java
@@ -0,0 +1,37 @@
1package lv.enes.orang.value;
2
3import lv.enes.orang.OrangRuntimeException;
4import lv.enes.orang.Scope;
5import lv.enes.orang.ast.Expression;
6
7import java.util.ArrayList;
8import java.util.Collections;
9import java.util.HashMap;
10import java.util.List;
11
12public record PartialFunction(Scope scope, List<String> args, List<Value> params, Expression body) implements Value {
13 @Override
14 public String typeName() {
15 return "Function";
16 }
17
18 @Override
19 public String stringify() {
20 return "#function";
21 }
22
23 @Override
24 public Value call(Value param) throws OrangRuntimeException {
25 var newParams = new ArrayList<>(params);
26 newParams.add(param);
27 if (newParams.size() == args.size()) {
28 var newDefs = new HashMap<String, Value>();
29 for (var i = 0; i < args.size(); i++) {
30 newDefs.put(args.get(i), newParams.get(i));
31 }
32 return body.evaluateExpression(scope.withDefinitions(newDefs));
33 }
34
35 return new PartialFunction(scope, args, Collections.unmodifiableList(newParams), body);
36 }
37}
diff --git a/src/main/java/lv/enes/orang/value/Undefined.java b/src/main/java/lv/enes/orang/value/Undefined.java
new file mode 100644
index 0000000..a341ee8
--- /dev/null
+++ b/src/main/java/lv/enes/orang/value/Undefined.java
@@ -0,0 +1,21 @@
1package lv.enes.orang.value;
2
3import lombok.EqualsAndHashCode;
4
5@EqualsAndHashCode
6public final class Undefined implements Value {
7 public static final Undefined INSTANCE = new Undefined();
8
9 private Undefined() {
10 }
11
12 @Override
13 public String typeName() {
14 return "Undefined";
15 }
16
17 @Override
18 public String stringify() {
19 return "#undefined";
20 }
21}
diff --git a/src/main/java/lv/enes/orang/value/Value.java b/src/main/java/lv/enes/orang/value/Value.java
new file mode 100644
index 0000000..fa8275c
--- /dev/null
+++ b/src/main/java/lv/enes/orang/value/Value.java
@@ -0,0 +1,77 @@
1package lv.enes.orang.value;
2
3import lv.enes.orang.OrangRuntimeException;
4
5public sealed interface Value
6 permits Array, BuiltinFunction, Function, OrangBoolean, OrangInteger, OrangString, PartialBuiltinFunction,
7 PartialFunction, Undefined {
8 String typeName();
9 String stringify();
10
11 default String display() {
12 return stringify();
13 }
14
15
16 default Value negate() throws OrangRuntimeException {
17 throw new OrangRuntimeException(STR."negate is not implemented for \{typeName()}");
18 }
19
20 default Value not() throws OrangRuntimeException {
21 throw new OrangRuntimeException(STR."not is not implemented for \{typeName()}");
22 }
23
24 default Value plus() throws OrangRuntimeException {
25 throw new OrangRuntimeException(STR."plus is not implemented for \{typeName()}");
26 }
27
28
29 default Value add(Value rhs) throws OrangRuntimeException {
30 throw new OrangRuntimeException(STR."add is not implemented for \{typeName()} and \{rhs.typeName()}");
31 }
32
33 default Value call(Value rhs) throws OrangRuntimeException {
34 throw new OrangRuntimeException(STR."call is not implemented for \{typeName()} and \{rhs.typeName()}");
35 }
36
37 default Value divide(Value rhs) throws OrangRuntimeException {
38 throw new OrangRuntimeException(STR."divide is not implemented for \{typeName()} and \{rhs.typeName()}");
39 }
40
41 default Value multiply(Value rhs) throws OrangRuntimeException {
42 throw new OrangRuntimeException(STR."multiply is not implemented for \{typeName()} and \{rhs.typeName()}");
43 }
44
45 default Value or(Value rhs) throws OrangRuntimeException {
46 throw new OrangRuntimeException(STR."or is not implemented for \{typeName()} and \{rhs.typeName()}");
47 }
48
49 default Value subtract(Value rhs) throws OrangRuntimeException {
50 throw new OrangRuntimeException(STR."subtract is not implemented for \{typeName()} and \{rhs.typeName()}");
51 }
52
53
54 default OrangBoolean greaterThan(Value rhs) throws OrangRuntimeException {
55 throw new OrangRuntimeException(STR."greater than is not implemented for \{typeName()} and \{rhs.typeName()}");
56 }
57
58 default OrangBoolean greaterThanOrEqual(Value rhs) throws OrangRuntimeException {
59 return greaterThan(rhs).or(orangEquals(rhs));
60 }
61
62 default OrangBoolean lessThan(Value rhs) throws OrangRuntimeException {
63 return greaterThanOrEqual(rhs).not();
64 }
65
66 default OrangBoolean lessThanOrEqual(Value rhs) throws OrangRuntimeException {
67 return greaterThan(rhs).not();
68 }
69
70 default OrangBoolean orangEquals(Value rhs) {
71 return OrangBoolean.of(this.equals(rhs));
72 }
73
74 default OrangBoolean notEquals(Value rhs) {
75 return orangEquals(rhs).not();
76 }
77}
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
new file mode 100644
index 0000000..a4bdb66
--- /dev/null
+++ b/src/main/java/module-info.java
@@ -0,0 +1,9 @@
1module lv.enes.orang {
2 exports lv.enes.orang;
3
4 requires static jakarta.annotation;
5 requires static lombok;
6
7 requires org.slf4j;
8 requires org.slf4j.simple;
9} \ No newline at end of file
diff --git a/src/main/resources/lv/enes/orang/prelude.orang b/src/main/resources/lv/enes/orang/prelude.orang
new file mode 100644
index 0000000..36bf197
--- /dev/null
+++ b/src/main/resources/lv/enes/orang/prelude.orang
@@ -0,0 +1,20 @@
1def isRepl = __builtin_isRepl ()
2def len arrayOrString = __builtin_len arrayOrString
3def parseInt stringOrInt = __builtin_parseInt stringOrInt
4def print anything = __builtin_print anything
5def printLn x = do print x; print "\n"; x end
6def randInt min max = __builtin_randInt min max
7def readInt _ = parseInt (readLn ())
8def readLn _ = __builtin_readLn ()
9
10def _ =
11 if isRepl then do
12 printLn "Hello! This is the Orang Programming Language!";
13 printLn "You can execute code right here at the prompt.";
14 printLn "If you wish to load & execute a file, use `:l filename.orang'.";
15 printLn "If you wish to quit, use `:q'.";
16 printLn "Have a nice day :)";
17 printLn ""
18 end else
19 ()
20