diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
new file mode 100644
index 00000000..d59d51fe
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -0,0 +1,74 @@
+name: Bug Report
+description: Create a bug-report to help us address errors and bugs.
+title: "[BUG]"
+labels: [bug]
+body:
+- type: checkboxes
+ attributes:
+ label: Is there an existing issue for this?
+ description: Please search to see if an issue already exists for the bug you encountered.
+ options:
+ - label: I have searched the existing issues
+ required: true
+- type: textarea
+ attributes:
+ label: Current Behavior
+ description: A concise description of what you're experiencing.
+ validations:
+ required: false
+- type: textarea
+ attributes:
+ label: Expected Behavior
+ description: A concise description of what you expected to happen.
+ validations:
+ required: false
+- type: textarea
+ attributes:
+ label: Steps To Reproduce
+ description: Steps to reproduce the behavior.
+ placeholder: |
+ 1. In this environment...
+ 2. With this config...
+ 3. Run '...'
+ 4. See error...
+ validations:
+ required: false
+- type: textarea
+ attributes:
+ label: Anything else?
+ description: |
+ Links? References? Anything that will give us more context about the issue you are encountering!
+
+ Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
+ validations:
+ required: false
+
+---
+name: Bug Report
+about: Create a bug-report to help us address errors and bugs.
+title: 'BUG'
+labels: "bug"
+
+---
+
+**Description**
+
+
+
+**Error Code:**
+
+Error Message Displayed:
+
+**Working Environment**
+
+Operating System (eg Linux):
+
+Dictu Version:
+
+**Screenshots**
+
+Please add a screenshot if applicable
+
+[Optional] **Additional Context**
+
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..0086358d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: true
diff --git a/.github/ISSUE_TEMPLATE/feature-report.yml b/.github/ISSUE_TEMPLATE/feature-report.yml
new file mode 100644
index 00000000..db950f44
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-report.yml
@@ -0,0 +1,49 @@
+name: Feature Request
+description: Suggest an idea for this project.
+title: "[FEATURE]"
+labels: [feature request]
+body:
+- type: checkboxes
+ attributes:
+ label: Is there an existing issue for this?
+ description: Please search to see if an issue already exists for the feature you are requesting.
+ options:
+ - label: I have searched the existing issues
+ required: true
+- type: textarea
+ attributes:
+ label: Is your feature request related to a problem?
+ description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+ validations:
+ required: false
+- type: textarea
+ attributes:
+ label: Describe the solution you'd like
+ description: A clear and concise description of what you want to happen.
+ validations:
+ required: false
+- type: textarea
+ attributes:
+ label: Describe alternatives you've considered
+ description: A clear and concise description of any alternative solutions or features you've considered.
+ validations:
+ required: false
+- type: textarea
+ attributes:
+ label: Additional context
+ description: Add any other context such as screenshots, schematics, about the feature request here.
+ validations:
+ required: false
+
+---
+name: Feature Request
+about: Suggest an idea for this project
+title: '[FEATURE]'
+labels: "feature request"
+
+---
+name: Feature/Enhancement request
+about: Suggest an idea for this project.
+title: "[FEATURE]"
+labels: feature request
+---
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..380d07e7
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+### Well detailed description of the change :
+
+
+
+ I worked on the .....
+
+#
+
+### Context of the change :
+
+
+
+ - Why is this change required ?
+
+
+
+- Does it solve a problem ? (please link the issue)
+
+#
+
+### Type of change :
+
+
+
+
+
+
+
+
+- [ ] Bug fix
+
+- [ ] New feature
+
+- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
+
+#
+
+### Preview (Screenshots) :
+
+
+
+
+
+
If it is possible, please link screenshots of your changes preview !
+
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 25af0e73..15e93f0f 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [ubuntu-latest, ubuntu-16.04, ubuntu-20.04]
+ os: [ubuntu-latest, ubuntu-20.04]
steps:
- uses: actions/checkout@v2
@@ -36,7 +36,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [macOS-latest]
+ os: [macOS-latest, macOS-11]
steps:
- uses: actions/checkout@v2
@@ -66,4 +66,16 @@ jobs:
run: |
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_SYSTEM_VERSION="10.0.18362.0" -DCICD=1 -DDISABLE_HTTP=1 -DDISABLE_LINENOISE=1 -B build
cmake --build build
- Debug\dictu.exe tests/runTests.du
\ No newline at end of file
+ Debug\dictu.exe tests/runTests.du
+ run-examples:
+ name: Test Examples
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Make dictu and run examples
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libcurl4-openssl-dev
+ cmake -DCMAKE_BUILD_TYPE=Debug -B ./build
+ cmake --build ./build
+ ./dictu examples/runExamples.du | tee /dev/stderr | grep -q 'Total memory usage: 0'
diff --git a/docs/_config.yml b/docs/_config.yml
index 6871e498..2b0df27e 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -5,7 +5,7 @@ description: >-
color_scheme: "dictu" # Custom theme
logo: "/assets/images/dictu-logo/dictu-wordmark.svg"
-version: "0.20.0"
+version: "0.21.0"
github_username: dictu-lang
search_enabled: true
diff --git a/docs/docs/built-ins.md b/docs/docs/built-ins.md
index 5c85fb10..95009a7f 100644
--- a/docs/docs/built-ins.md
+++ b/docs/docs/built-ins.md
@@ -35,6 +35,16 @@ print("test"); // "test"
print(10, "test", nil, true); // 10, "test", nil, true
```
+### printError(...values...)
+
+Prints a given list of values to stderr.
+
+```cs
+printError(10); // 10
+printError("test"); // "test"
+printError(10, "test", nil, true); // 10, "test", nil, true
+```
+
### input(string: prompt -> optional)
Gathers user input from stdin and returns the value as a string. `input()` has an optional prompt which will be shown to
diff --git a/docs/docs/classes.md b/docs/docs/classes.md
index ab5d741d..89f51b72 100644
--- a/docs/docs/classes.md
+++ b/docs/docs/classes.md
@@ -561,4 +561,47 @@ var anotherTestObj = AnotherTest();
testObj.isInstance(AnotherTest); // false
anotherTestObj.isInstance(AnotherTest); // true
anotherTestObj.isInstance(Test); // true
-```
\ No newline at end of file
+```
+
+## Annotations
+
+Annotations are metadata that are applied to classes that by themselves have no impact.
+They, however, can provide user defined changes at runtime to given classes.
+
+```cs
+@Annotation
+class AnnotatedClass {
+
+}
+```
+
+Annotations are accessed via the `.annotations` property available on all classes. If annotations
+are preset a dictionary is returned, otherwise the `.annotations` property is `nil`.
+
+```cs
+print(AnnotatedClass.annotations); // {"Annotation": nil}
+```
+
+Annotations can also be supplied a value, however, the value must be of type: nil, boolean, number or string.
+
+```
+@Annotation("Some extra value!")
+class AnnotatedClass {
+
+}
+
+print(AnnotatedClass.annotations); // {"Annotation": "Some extra value!"}
+```
+
+Multiple annotations can be supplied to classes.
+
+```cs
+@Annotation
+@AnotherAnnotation(10)
+@SomeOtherAnnotation
+class AnnotatedClass {
+
+}
+```
+
+**Note**: Annotations are not available on methods.
\ No newline at end of file
diff --git a/docs/docs/standard-lib/math.md b/docs/docs/standard-lib/math.md
index aade9a35..ee72baa9 100644
--- a/docs/docs/standard-lib/math.md
+++ b/docs/docs/standard-lib/math.md
@@ -143,3 +143,21 @@ Returns the tan value of a given number in radian
Math.tan(1); // 1.5574
Math.tan(50); // -0.2719
```
+
+### Math.gcd(iterable)
+
+Return the greatest common divisor of the numbers within the iterable
+
+```cs
+Math.gcd(32, 24, 12); // 4
+Math.gcd([32, 24, 12]); // 4
+```
+
+### Math.lcm(iterable)
+
+Return the least common multiple of the numbers within the iterable
+
+```cs
+Math.lcm(32, 24, 12); // 96
+Math.lcm([32, 24, 12]); // 96
+```
diff --git a/docs/docs/standard-lib/path.md b/docs/docs/standard-lib/path.md
index 684ea25b..70b9d6f0 100644
--- a/docs/docs/standard-lib/path.md
+++ b/docs/docs/standard-lib/path.md
@@ -102,3 +102,15 @@ Returns a list of strings containing the contents of the input path.
```js
Path.listDir("/"); // ["bin", "dev", "home", "lib", ...]
```
+
+### Path.join(iterable)
+
+Returns the provided string arguments joined using the directory separator.
+
+**Note:** A trailing directory separator is ignored from each argument
+
+```js
+Path.join('/tmp', 'abcd', 'efg') == '/tmp/abcd/efg';
+Path.join(['/tmp', 'abcd', 'efg']) == '/tmp/abcd/efg';
+Path.join('/tmp/', 'abcd/', 'efg/') == '/tmp/abcd/efg';
+```
diff --git a/docs/docs/strings.md b/docs/docs/strings.md
index 38f627af..95131dc9 100644
--- a/docs/docs/strings.md
+++ b/docs/docs/strings.md
@@ -242,3 +242,13 @@ Returns the number of occurrences of a given substring within another string.
"This documentation".count("Good jokes"); // 0
"Sooooooooooome characters".count("o"); // 11
```
+
+### string.title()
+
+Returns a title cased version of string with first letter of each word capitalized.
+
+```cs
+"dictu language".title(); // Dictu Language
+"this documentation".title(); // This Documentation
+"once upon a time".title(); // Once Upon A Time
+```
\ No newline at end of file
diff --git a/examples/factorial.du b/examples/factorial.du
index d1adf354..a3ab8052 100644
--- a/examples/factorial.du
+++ b/examples/factorial.du
@@ -1,4 +1,14 @@
-var amount = input("Enter a number: ").toNumber().unwrap();
+var amount;
+
+/**
+ * This is to handle the case where the script is ran headless in the CI/CD pipeline.
+ */
+if (System.argv[0] == __file__) {
+ amount = input("Enter a number: ").toNumber().unwrap();
+} else {
+ amount = 8;
+}
+
var num = 1;
if (amount > 0) {
diff --git a/examples/fibonacci.du b/examples/fibonacci.du
new file mode 100644
index 00000000..f31f5071
--- /dev/null
+++ b/examples/fibonacci.du
@@ -0,0 +1,27 @@
+def iterative_fibonacci(n) {
+ var a = 0, b = 1, c = 0;
+ for (var i = 1; i < n; i += 1) {
+ c = a;
+ a = b;
+ b = b + c;
+ }
+ return b;
+}
+
+var memoization_dict = {};
+def memoized_recursive_fibonacci(n) {
+ if (memoization_dict.exists(n)) return memoization_dict[n];
+ if (n < 2) return n;
+ var result = memoized_recursive_fibonacci(n - 2) + memoized_recursive_fibonacci(n - 1);
+ memoization_dict[n] = result;
+ return result;
+}
+
+def recursive_fibonacci(n) {
+ if (n < 2) return n;
+ return recursive_fibonacci(n - 2) + recursive_fibonacci(n - 1);
+}
+
+print('iterative_fibonacci(100) = {}'.format(iterative_fibonacci(100)));
+print('memoized_recursive_fibonacci(100) = {}'.format(memoized_recursive_fibonacci(100)));
+print('recursive_fibonacci(10) = {}'.format(recursive_fibonacci(10)));
diff --git a/examples/guessingGame.du b/examples/guessingGame.du
index 6e20c9b0..59a775e9 100644
--- a/examples/guessingGame.du
+++ b/examples/guessingGame.du
@@ -1,7 +1,20 @@
+import Random;
+
const guess = 10;
+var maxGuesses = 5;
while {
- const userInput = input("Input your guess: ").toNumber().unwrap();
+ var userInput;
+
+ /**
+ * This is to handle the case where the script is ran headless in the CI/CD pipeline.
+ */
+ if (System.argv[0] == __file__) {
+ userInput = input("Input your guess: ").toNumber().unwrap();
+ } else {
+ userInput = Random.range(7, 12);
+ print("Automated guess: {}".format(userInput));
+ }
if (userInput == guess) {
print("Well done!");
break;
@@ -12,4 +25,10 @@ while {
}
System.sleep(1);
+ maxGuesses -= 1;
+
+ if (maxGuesses <= 0) {
+ print("You ran out of guesses, the answer was {}!".format(guess));
+ break;
+ }
}
\ No newline at end of file
diff --git a/examples/recursiveFib.du b/examples/recursiveFib.du
deleted file mode 100644
index 5ad0c13c..00000000
--- a/examples/recursiveFib.du
+++ /dev/null
@@ -1,9 +0,0 @@
-def fibonacci(num) {
- if (num < 2) {
- return num;
- }
-
- return fibonacci(num - 2) + fibonacci(num - 1);
-}
-
-print(fibonacci(10));
\ No newline at end of file
diff --git a/examples/runExamples.du b/examples/runExamples.du
new file mode 100644
index 00000000..a8bb52fe
--- /dev/null
+++ b/examples/runExamples.du
@@ -0,0 +1,18 @@
+/**
+ * File for the CI/CD pipeline to run all the examples.
+ */
+
+import "design-patterns/builder.du";
+import "design-patterns/chain-of-responsibility.du";
+import "design-patterns/factoryMethod.du";
+import "design-patterns/observer.du";
+import "binarySearchTree.du";
+import "bubbleSort.du";
+import "classes.du";
+import "factorial.du";
+import "fibonacci.du";
+import "fizzBuzz.du";
+import "guessingGame.du";
+import "inheritance.du";
+import "isPalindrome.du";
+import "factorial.du";
diff --git a/src/include/dictu_include.h b/src/include/dictu_include.h
index 67980dfc..4a51593d 100644
--- a/src/include/dictu_include.h
+++ b/src/include/dictu_include.h
@@ -4,7 +4,7 @@
#include
#define DICTU_MAJOR_VERSION "0"
-#define DICTU_MINOR_VERSION "20"
+#define DICTU_MINOR_VERSION "21"
#define DICTU_PATCH_VERSION "0"
#define DICTU_STRING_VERSION "Dictu Version: " DICTU_MAJOR_VERSION "." DICTU_MINOR_VERSION "." DICTU_PATCH_VERSION "\n"
diff --git a/src/optionals/math.c b/src/optionals/math.c
index d17fa194..d091566d 100644
--- a/src/optionals/math.c
+++ b/src/optionals/math.c
@@ -221,6 +221,112 @@ static Value tanNative(DictuVM *vm, int argCount, Value *args) {
return NUMBER_VAL(tan(AS_NUMBER(args[0])));
}
+static long long gcd(long long a, long long b) {
+ long long r;
+ while (b > 0) {
+ r = a % b;
+ a = b;
+ b = r;
+ }
+ return a;
+}
+
+static Value gcdNative(DictuVM *vm, int argCount, Value *args) {
+ char* argCountError = "gcd() requires 2 or more arguments (%d given).";
+ char* nonNumberError = "gcd() argument at index %d is not a number";
+ char* notWholeError = "gcd() argument (%f) at index %d is not a whole number";
+
+ if (argCount == 1 && IS_LIST(args[0])) {
+ argCountError = "List passed to gcd() must have 2 or more elements (%d given).";
+ nonNumberError = "The element at index %d of the list passed to gcd() is not a number";
+ notWholeError = "The element (%f) at index %d of the list passed to gcd() is not a whole number";
+ ObjList *list = AS_LIST(args[0]);
+ argCount = list->values.count;
+ args = list->values.values;
+ }
+
+ if (argCount < 2) {
+ runtimeError(vm, argCountError, argCount);
+ return EMPTY_VAL;
+ }
+
+ for (int i = 0; i < argCount; ++i)
+ if (!IS_NUMBER(args[i])) {
+ runtimeError(vm, nonNumberError, i);
+ return EMPTY_VAL;
+ }
+
+ double* as_doubles = ALLOCATE(vm, double, argCount);
+ for (int i = 0; i < argCount; ++i) {
+ as_doubles[i] = AS_NUMBER(args[i]);
+ if (fabs(round(as_doubles[i]) - as_doubles[i]) > FLOAT_TOLERANCE) {
+ runtimeError(vm, notWholeError, as_doubles[i], i);
+ FREE_ARRAY(vm, double, as_doubles, argCount);
+ return EMPTY_VAL;
+ }
+ }
+
+ long long* as_longlongs = ALLOCATE(vm, long long, argCount);
+ for (int i = 0; i < argCount; ++i) as_longlongs[i] = round(as_doubles[i]);
+
+ long long result = as_longlongs[0];
+ for (int i = 1; i < argCount; ++i) result = gcd(result, as_longlongs[i]);
+
+ FREE_ARRAY(vm, double, as_doubles, argCount);
+ FREE_ARRAY(vm, long long, as_longlongs, argCount);
+ return NUMBER_VAL(result);
+}
+
+long long lcm(long long a, long long b) {
+ return (a * b) / gcd(a, b);
+}
+
+static Value lcmNative(DictuVM *vm, int argCount, Value *args) {
+ char* argCountError = "lcm() requires 2 or more arguments (%d given).";
+ char* nonNumberError = "lcm() argument at index %d is not a number";
+ char* notWholeError = "lcm() argument (%f) at index %d is not a whole number";
+
+ if (argCount == 1 && IS_LIST(args[0])) {
+ argCountError = "List passed to lcm() must have 2 or more elements (%d given).";
+ nonNumberError = "The element at index %d of the list passed to lcm() is not a number";
+ notWholeError = "The element (%f) at index %d of the list passed to lcm() is not a whole number";
+ ObjList *list = AS_LIST(args[0]);
+ argCount = list->values.count;
+ args = list->values.values;
+ }
+
+ if (argCount < 2) {
+ runtimeError(vm, argCountError, argCount);
+ return EMPTY_VAL;
+ }
+
+ for (int i = 0; i < argCount; ++i)
+ if (!IS_NUMBER(args[i])) {
+ runtimeError(vm, nonNumberError, i);
+ return EMPTY_VAL;
+ }
+
+ double* as_doubles = ALLOCATE(vm, double, argCount);
+ for (int i = 0; i < argCount; ++i) {
+ as_doubles[i] = AS_NUMBER(args[i]);
+ if (fabs(round(as_doubles[i]) - as_doubles[i]) > FLOAT_TOLERANCE) {
+ runtimeError(vm, notWholeError, as_doubles[i], i);
+ FREE_ARRAY(vm, double, as_doubles, argCount);
+ return EMPTY_VAL;
+ }
+ }
+
+ long long* as_longlongs = ALLOCATE(vm, long long, argCount);
+ for (int i = 0; i < argCount; ++i) as_longlongs[i] = round(as_doubles[i]);
+
+ long long result = as_longlongs[0];
+ for (int i = 1; i < argCount; ++i) result = lcm(result, as_longlongs[i]);
+
+ FREE_ARRAY(vm, double, as_doubles, argCount);
+ FREE_ARRAY(vm, long long, as_longlongs, argCount);
+ return NUMBER_VAL(result);
+}
+
ObjModule *createMathsModule(DictuVM *vm) {
ObjString *name = copyString(vm, "Math", 4);
push(vm, OBJ_VAL(name));
@@ -242,6 +348,8 @@ ObjModule *createMathsModule(DictuVM *vm) {
defineNative(vm, &module->values, "sin", sinNative);
defineNative(vm, &module->values, "cos", cosNative);
defineNative(vm, &module->values, "tan", tanNative);
+ defineNative(vm, &module->values, "gcd", gcdNative);
+ defineNative(vm, &module->values, "lcm", lcmNative);
/**
* Define Math properties
diff --git a/src/optionals/math.h b/src/optionals/math.h
index 5336d9bc..46b0aca6 100644
--- a/src/optionals/math.h
+++ b/src/optionals/math.h
@@ -7,6 +7,8 @@
#include "optionals.h"
#include "../vm/vm.h"
+#define FLOAT_TOLERANCE 0.00001
+
ObjModule *createMathsModule(DictuVM *vm);
#endif //dictu_math_h
diff --git a/src/optionals/path.c b/src/optionals/path.c
index 4307d309..7833ab73 100644
--- a/src/optionals/path.c
+++ b/src/optionals/path.c
@@ -233,6 +233,85 @@ static Value listDirNative(DictuVM *vm, int argCount, Value *args) {
return OBJ_VAL(dir_contents);
}
+static Value joinNative(DictuVM *vm, int argCount, Value *args) {
+ char* argCountError = "join() requires 1 or more arguments (%d given).";
+ char* nonStringError = "join() argument at index %d is not a string";
+
+ if (argCount == 1 && IS_LIST(args[0])) {
+ argCountError = "List passed to join() must have 1 or more elements (%d given).";
+ nonStringError = "The element at index %d of the list passed to join() is not a string";
+ ObjList *list = AS_LIST(args[0]);
+ argCount = list->values.count;
+ args = list->values.values;
+ }
+
+ if (argCount == 0) {
+ runtimeError(vm, argCountError, argCount);
+ return EMPTY_VAL;
+ }
+
+ for (int i = 0; i < argCount; ++i) {
+ if (!IS_STRING(args[i])) {
+ runtimeError(vm, nonStringError, i);
+ return EMPTY_VAL;
+ }
+ }
+
+ ObjString* part;
+ // resultSize = # of dir separators that will be used + length of each string arg
+ size_t resultSize = abs(argCount - 1); // abs is needed here because of a clang bug
+ for (int i = 0; i < argCount; ++i) {
+ part = AS_STRING(args[i]);
+ resultSize += part->length;
+ // Account for leading DIR_SEPARATOR chars to be removed
+ for (int j = 0; j < part->length; ++j) {
+ if (part->chars[j] == DIR_SEPARATOR) --resultSize;
+ else break;
+ }
+ // Account for trailing DIR_SEPARATOR chars to be removed
+ for (int j = part->length - 1; j >= 0; --j) {
+ if (part->chars[j] == DIR_SEPARATOR) --resultSize;
+ else break;
+ }
+ }
+ // Account for leading/trailing DIR_SEPARATOR on the first/last part respectively
+ part = AS_STRING(args[0]);
+ resultSize += part->chars[0] == DIR_SEPARATOR;
+ part = AS_STRING(args[argCount - 1]);
+ resultSize += part->chars[part->length - 1] == DIR_SEPARATOR;
+
+ char* str = ALLOCATE(vm, char, resultSize + 1);
+ char* dest = str;
+ for (int i = 0; i < argCount; ++i) {
+ part = AS_STRING(args[i]);
+
+ // Skip leading DIR_SEPARATOR characters on everything except for one on the first part,
+ // and trailing DIR_SEPARATOR characters on everything except for one on the last part.
+ // e.g. `join('///tmp///', '/abc///')` returns '/tmp/abc' instead of '///tmp////abc///'
+ int start = 0;
+ while (part->chars[start++] == DIR_SEPARATOR);
+ int end = part->length - 1;
+ while (part->chars[end--] == DIR_SEPARATOR);
+
+ bool lastIteration = i == argCount - 1;
+
+ // Check if the first part starts with a DIR_SEPARATOR so that we can preserve it
+ bool firstLeadingDirSep = !i && part->chars[0] == DIR_SEPARATOR;
+
+ // Check if the last part ends with a DIR_SEPARATOR so that we can preserve it
+ bool lastTrailingDirSep = lastIteration && part->chars[part->length - 1] == DIR_SEPARATOR;
+
+ // Append the part string to the end of dest
+ for (int j = start - 1 - firstLeadingDirSep; j <= end + 1 + lastTrailingDirSep; ++j)
+ *dest++ = part->chars[j];
+
+ // Append a DIR_SEPARATOR if necessary
+ if (!lastIteration && part->chars[end + 1] != DIR_SEPARATOR) *dest++ = DIR_SEPARATOR;
+ }
+
+ return OBJ_VAL(takeString(vm, str, resultSize));
+}
+
ObjModule *createPathModule(DictuVM *vm) {
ObjString *name = copyString(vm, "Path", 4);
push(vm, OBJ_VAL(name));
@@ -252,6 +331,7 @@ ObjModule *createPathModule(DictuVM *vm) {
defineNative(vm, &module->values, "exists", existsNative);
defineNative(vm, &module->values, "isDir", isdirNative);
defineNative(vm, &module->values, "listDir", listDirNative);
+ defineNative(vm, &module->values, "join", joinNative);
/**
* Define Path properties
diff --git a/src/vm/compiler.c b/src/vm/compiler.c
index 70741f7a..3f21a925 100644
--- a/src/vm/compiler.c
+++ b/src/vm/compiler.c
@@ -157,6 +157,7 @@ static void initCompiler(Parser *parser, Compiler *compiler, Compiler *parent, F
compiler->class = NULL;
compiler->loop = NULL;
compiler->withBlock = false;
+ compiler->annotations = NULL;
if (parent != NULL) {
compiler->class = parent->class;
@@ -919,7 +920,7 @@ static void grouping(Compiler *compiler, bool canAssign) {
consume(compiler, TOKEN_RIGHT_PAREN, "Expect ')' after expression.");
}
-static void number(Compiler *compiler, bool canAssign) {
+static Value parseNumber(Compiler *compiler, bool canAssign) {
UNUSED(canAssign);
// We allocate the whole range for the worst case.
@@ -941,10 +942,14 @@ static void number(Compiler *compiler, bool canAssign) {
// Parse the string.
double value = strtod(buffer, NULL);
- emitConstant(compiler, NUMBER_VAL(value));
-
// Free the malloc'd buffer.
FREE_ARRAY(compiler->parser->vm, char, buffer, compiler->parser->previous.length + 1);
+
+ return NUMBER_VAL(value);
+}
+
+static void number(Compiler *compiler, bool canAssign) {
+ emitConstant(compiler, parseNumber(compiler, canAssign));
}
static void or_(Compiler *compiler, Token previousToken, bool canAssign) {
@@ -975,7 +980,7 @@ static void or_(Compiler *compiler, Token previousToken, bool canAssign) {
patchJump(compiler, endJump);
}
-int parseString(char *string, int length) {
+int parseEscapeSequences(char *string, int length) {
for (int i = 0; i < length - 1; i++) {
if (string[i] == '\\') {
switch (string[i + 1]) {
@@ -1029,7 +1034,7 @@ static void rString(Compiler *compiler, bool canAssign) {
consume(compiler, TOKEN_STRING, "Expected string after r delimiter");
}
-static void string(Compiler *compiler, bool canAssign) {
+static Value parseString(Compiler *compiler, bool canAssign) {
UNUSED(canAssign);
Parser *parser = compiler->parser;
@@ -1038,7 +1043,7 @@ static void string(Compiler *compiler, bool canAssign) {
char *string = ALLOCATE(parser->vm, char, stringLength + 1);
memcpy(string, parser->previous.start + 1, stringLength);
- int length = parseString(string, stringLength);
+ int length = parseEscapeSequences(string, stringLength);
// If there were escape chars and the string shrank, resize the buffer
if (length != stringLength) {
@@ -1046,7 +1051,11 @@ static void string(Compiler *compiler, bool canAssign) {
}
string[length] = '\0';
- emitConstant(compiler, OBJ_VAL(takeString(parser->vm, string, length)));
+ return OBJ_VAL(takeString(parser->vm, string, length));
+}
+
+static void string(Compiler *compiler, bool canAssign) {
+ emitConstant(compiler, parseString(compiler, canAssign));
}
static void list(Compiler *compiler, bool canAssign) {
@@ -1412,6 +1421,7 @@ ParseRule rules[] = {
{NULL, binary, PREC_FACTOR}, // TOKEN_STAR
{NULL, binary, PREC_INDICES}, // TOKEN_STAR_STAR
{NULL, binary, PREC_FACTOR}, // TOKEN_PERCENT
+ {NULL, NULL, PREC_NONE}, // TOKEN_AT
{NULL, binary, PREC_BITWISE_AND}, // TOKEN_AMPERSAND
{NULL, binary, PREC_BITWISE_XOR}, // TOKEN_CARET
{NULL, binary, PREC_BITWISE_OR}, // TOKEN_PIPE
@@ -1577,6 +1587,7 @@ static void setupClassCompiler(Compiler *compiler, ClassCompiler *classCompiler,
classCompiler->enclosing = compiler->class;
classCompiler->staticMethod = false;
classCompiler->abstractClass = abstract;
+ classCompiler->annotations = NULL;
initTable(&classCompiler->privateVariables);
compiler->class = classCompiler;
}
@@ -1584,6 +1595,61 @@ static void setupClassCompiler(Compiler *compiler, ClassCompiler *classCompiler,
static void endClassCompiler(Compiler *compiler, ClassCompiler *classCompiler) {
freeTable(compiler->parser->vm, &classCompiler->privateVariables);
compiler->class = compiler->class->enclosing;
+
+ if (compiler->annotations != NULL) {
+ int importConstant = makeConstant(compiler, OBJ_VAL(compiler->annotations));
+ emitBytes(compiler, OP_DEFINE_CLASS_ANNOTATIONS, importConstant);
+ compiler->annotations = NULL;
+ }
+}
+
+static bool checkLiteralToken(Compiler *compiler) {
+ return check(compiler, TOKEN_STRING) || check(compiler, TOKEN_NUMBER) ||
+ check(compiler, TOKEN_TRUE) || check(compiler, TOKEN_FALSE) || check(compiler, TOKEN_NIL);
+}
+
+static void parseClassAnnotations(Compiler *compiler) {
+ DictuVM *vm = compiler->parser->vm;
+ compiler->annotations = newDict(vm);
+
+ do {
+ consume(compiler, TOKEN_IDENTIFIER, "Expected annotation identifier");
+ Value annotationName = OBJ_VAL(copyString(vm, compiler->parser->previous.start,
+ compiler->parser->previous.length));
+ push(vm, annotationName);
+
+ if (match(compiler, TOKEN_LEFT_PAREN)) {
+ if (!checkLiteralToken(compiler)) {
+ errorAtCurrent(compiler->parser,
+ "Annotations can only have literal values of type string, bool, number or nil.");
+ return;
+ }
+
+ if (match(compiler, TOKEN_STRING)) {
+ Value string = parseString(compiler, false);
+ push(vm, string);
+ dictSet(vm, compiler->annotations, annotationName, string);
+ pop(vm);
+ } else if (match(compiler, TOKEN_NUMBER)) {
+ Value number = parseNumber(compiler, false);
+ dictSet(vm, compiler->annotations, annotationName, number);
+ } else if (match(compiler, TOKEN_TRUE)) {
+ dictSet(vm, compiler->annotations, annotationName, TRUE_VAL);
+ } else if (match(compiler, TOKEN_FALSE)) {
+ dictSet(vm, compiler->annotations, annotationName, FALSE_VAL);
+ } else if (match(compiler, TOKEN_NIL)) {
+ dictSet(vm, compiler->annotations, annotationName, NIL_VAL);
+ } else {
+ dictSet(vm, compiler->annotations, annotationName, NIL_VAL);
+ }
+
+ consume(compiler, TOKEN_RIGHT_PAREN, "Expect ')' after annotation value.");
+ } else {
+ dictSet(vm, compiler->annotations, annotationName, NIL_VAL);
+ }
+
+ pop(vm);
+ } while (match(compiler, TOKEN_AT));
}
static void parseClassBody(Compiler *compiler) {
@@ -1599,23 +1665,25 @@ static void parseClassBody(Compiler *compiler) {
emitBytes(compiler, OP_SET_CLASS_VAR, name);
consume(compiler, TOKEN_SEMICOLON, "Expect ';' after variable declaration.");
- } else if (match(compiler, TOKEN_PRIVATE)) {
- if (match(compiler, TOKEN_IDENTIFIER)) {
- if (check(compiler, TOKEN_SEMICOLON)) {
- uint8_t name = identifierConstant(compiler, &compiler->parser->previous);
- consume(compiler, TOKEN_SEMICOLON, "Expect ';' after private variable declaration.");
- tableSet(compiler->parser->vm, &compiler->class->privateVariables,
- AS_STRING(currentChunk(compiler)->constants.values[name]), EMPTY_VAL);
+ } else {
+ if (match(compiler, TOKEN_PRIVATE)) {
+ if (match(compiler, TOKEN_IDENTIFIER)) {
+ if (check(compiler, TOKEN_SEMICOLON)) {
+ uint8_t name = identifierConstant(compiler, &compiler->parser->previous);
+ consume(compiler, TOKEN_SEMICOLON, "Expect ';' after private variable declaration.");
+ tableSet(compiler->parser->vm, &compiler->class->privateVariables,
+ AS_STRING(currentChunk(compiler)->constants.values[name]), EMPTY_VAL);
+ continue;
+ }
+
+ method(compiler, true, &compiler->parser->previous);
continue;
}
- method(compiler, true, &compiler->parser->previous);
- continue;
+ method(compiler, true, NULL);
+ } else {
+ method(compiler, false, NULL);
}
-
- method(compiler, true, NULL);
- } else {
- method(compiler, false, NULL);
}
}
}
@@ -1657,8 +1725,8 @@ static void classDeclaration(Compiler *compiler) {
emitByte(compiler, OP_END_CLASS);
}
- defineVariable(compiler, nameConstant, false);
endClassCompiler(compiler, &classCompiler);
+ defineVariable(compiler, nameConstant, false);
}
static void abstractClassDeclaration(Compiler *compiler) {
@@ -2330,7 +2398,15 @@ static void synchronize(Parser *parser) {
static void declaration(Compiler *compiler) {
if (match(compiler, TOKEN_CLASS)) {
classDeclaration(compiler);
- } else if (match(compiler, TOKEN_TRAIT)) {
+ if (compiler->parser->panicMode) synchronize(compiler->parser);
+ return;
+ }
+
+ if (compiler->annotations != NULL) {
+ errorAtCurrent(compiler->parser, "Annotations can only be applied to classes");
+ }
+
+ if (match(compiler, TOKEN_TRAIT)) {
traitDeclaration(compiler);
} else if (match(compiler, TOKEN_ABSTRACT)) {
abstractClassDeclaration(compiler);
@@ -2342,6 +2418,8 @@ static void declaration(Compiler *compiler) {
varDeclaration(compiler, true);
} else if (match(compiler, TOKEN_ENUM)) {
enumDeclaration(compiler);
+ } else if (match(compiler, TOKEN_AT)) {
+ parseClassAnnotations(compiler);
} else {
statement(compiler);
}
@@ -2454,10 +2532,12 @@ void grayCompilerRoots(DictuVM *vm) {
ClassCompiler *classCompiler = vm->compiler->class;
while (classCompiler != NULL) {
+ grayObject(vm, (Obj *) classCompiler->annotations);
grayTable(vm, &classCompiler->privateVariables);
classCompiler = classCompiler->enclosing;
}
+ grayObject(vm, (Obj *) compiler->annotations);
grayObject(vm, (Obj *) compiler->function);
grayTable(vm, &compiler->stringConstants);
compiler = compiler->enclosing;
diff --git a/src/vm/compiler.h b/src/vm/compiler.h
index 0e582fbc..e78329f6 100644
--- a/src/vm/compiler.h
+++ b/src/vm/compiler.h
@@ -63,6 +63,7 @@ typedef struct ClassCompiler {
bool staticMethod;
bool abstractClass;
Table privateVariables;
+ ObjDict *annotations;
} ClassCompiler;
typedef struct Loop {
@@ -101,6 +102,7 @@ typedef struct Compiler {
int scopeDepth;
bool withBlock;
+ ObjDict *annotations;
} Compiler;
typedef void (*ParsePrefixFn)(Compiler *compiler, bool canAssign);
diff --git a/src/vm/datatypes/instance.c b/src/vm/datatypes/instance.c
index 9b4dfd75..51aca3c8 100644
--- a/src/vm/datatypes/instance.c
+++ b/src/vm/datatypes/instance.c
@@ -65,6 +65,21 @@ static Value getAttribute(DictuVM *vm, int argCount, Value *args) {
return value;
}
+ if (tableGet(&instance->klass->publicMethods, AS_STRING(key), &value)) {
+ return value;
+ }
+
+ // Check class for properties
+ ObjClass *klass = instance->klass;
+
+ while (klass != NULL) {
+ if (tableGet(&klass->publicProperties, AS_STRING(key), &value)) {
+ return value;
+ }
+
+ klass = klass->superclass;
+ }
+
return defaultValue;
}
diff --git a/src/vm/datatypes/strings.c b/src/vm/datatypes/strings.c
index 8834b27f..604102b3 100644
--- a/src/vm/datatypes/strings.c
+++ b/src/vm/datatypes/strings.c
@@ -465,6 +465,34 @@ static Value countString(DictuVM *vm, int argCount, Value *args) {
return NUMBER_VAL(count);
}
+static Value titleString(DictuVM *vm, int argCount, Value *args) {
+ if (argCount != 0) {
+ runtimeError(vm, "title() takes no arguments (%d given)", argCount);
+ return EMPTY_VAL;
+ }
+
+ ObjString *string = AS_STRING(args[0]);
+ char *temp = ALLOCATE(vm, char, string->length + 1);
+
+ bool convertNext=true;
+
+ for (int i = 0; string->chars[i]; i++) {
+ if(string->chars[i]==' '){
+ convertNext=true;
+ }
+ else if(convertNext){
+ temp[i] = toupper(string->chars[i]);
+ convertNext=false;
+ continue;
+ }
+ temp[i] = tolower(string->chars[i]);
+ }
+
+ temp[string->length] = '\0';
+
+ return OBJ_VAL(takeString(vm, temp, string->length));
+}
+
void declareStringMethods(DictuVM *vm) {
defineNative(vm, &vm->stringMethods, "len", lenString);
defineNative(vm, &vm->stringMethods, "toNumber", toNumberString);
@@ -482,4 +510,5 @@ void declareStringMethods(DictuVM *vm) {
defineNative(vm, &vm->stringMethods, "strip", stripString);
defineNative(vm, &vm->stringMethods, "count", countString);
defineNative(vm, &vm->stringMethods, "toBool", boolNative); // Defined in util
+ defineNative(vm, &vm->stringMethods, "title", titleString);
}
diff --git a/src/vm/debug.c b/src/vm/debug.c
index b034298d..8c68184d 100644
--- a/src/vm/debug.c
+++ b/src/vm/debug.c
@@ -268,6 +268,8 @@ int disassembleInstruction(Chunk *chunk, int offset) {
return simpleInstruction("OP_END_CLASS", offset);
case OP_METHOD:
return constantInstruction("OP_METHOD", chunk, offset);
+ case OP_DEFINE_CLASS_ANNOTATIONS:
+ return constantInstruction("OP_DEFINE_CLASS_ANNOTATIONS", chunk, offset);
case OP_ENUM:
return constantInstruction("OP_ENUM", chunk, offset);
case OP_SET_ENUM_VALUE:
diff --git a/src/vm/memory.c b/src/vm/memory.c
index 1d2204af..81d47465 100644
--- a/src/vm/memory.c
+++ b/src/vm/memory.c
@@ -101,6 +101,7 @@ static void blackenObject(DictuVM *vm, Obj *object) {
ObjClass *klass = (ObjClass *) object;
grayObject(vm, (Obj *) klass->name);
grayObject(vm, (Obj *) klass->superclass);
+ grayObject(vm, (Obj *) klass->annotations);
grayTable(vm, &klass->publicMethods);
grayTable(vm, &klass->privateMethods);
grayTable(vm, &klass->abstractMethods);
diff --git a/src/vm/natives.c b/src/vm/natives.c
index 925064a9..eb402575 100644
--- a/src/vm/natives.c
+++ b/src/vm/natives.c
@@ -98,6 +98,22 @@ static Value printNative(DictuVM *vm, int argCount, Value *args) {
return NIL_VAL;
}
+static Value printErrorNative(DictuVM *vm, int argCount, Value *args) {
+ UNUSED(vm);
+
+ if (argCount == 0) {
+ fprintf(stderr, "\n");
+ return NIL_VAL;
+ }
+
+ for (int i = 0; i < argCount; ++i) {
+ printValueError(args[i]);
+ fprintf(stderr, "\n");
+ }
+
+ return NIL_VAL;
+}
+
static Value assertNative(DictuVM *vm, int argCount, Value *args) {
if (argCount != 1) {
runtimeError(vm, "assert() takes 1 argument (%d given)", argCount);
@@ -170,6 +186,7 @@ void defineAllNatives(DictuVM *vm) {
"type",
"set",
"print",
+ "printError",
"assert",
"isDefined",
"Success",
@@ -181,6 +198,7 @@ void defineAllNatives(DictuVM *vm) {
typeNative,
setNative,
printNative,
+ printErrorNative,
assertNative,
isDefinedNative,
generateSuccessResult,
diff --git a/src/vm/object.c b/src/vm/object.c
index 9ccc4ca0..cfc9ba45 100644
--- a/src/vm/object.c
+++ b/src/vm/object.c
@@ -68,6 +68,7 @@ ObjClass *newClass(DictuVM *vm, ObjString *name, ObjClass *superclass, ClassType
initTable(&klass->privateMethods);
initTable(&klass->publicMethods);
initTable(&klass->publicProperties);
+ klass->annotations = NULL;
return klass;
}
@@ -616,8 +617,9 @@ char *objectToString(Value value) {
case OBJ_STRING: {
ObjString *stringObj = AS_STRING(value);
- char *string = malloc(sizeof(char) * stringObj->length + 3);
- snprintf(string, stringObj->length + 3, "'%s'", stringObj->chars);
+ char *string = malloc(sizeof(char) * stringObj->length + 1);
+ memcpy(string, stringObj->chars, stringObj->length);
+ string[stringObj->length] = '\0';
return string;
}
diff --git a/src/vm/object.h b/src/vm/object.h
index 015f5972..885d8723 100644
--- a/src/vm/object.h
+++ b/src/vm/object.h
@@ -221,6 +221,7 @@ typedef struct sObjClass {
Table privateMethods;
Table abstractMethods;
Table publicProperties;
+ ObjDict *annotations;
ClassType type;
} ObjClass;
diff --git a/src/vm/opcodes.h b/src/vm/opcodes.h
index 7bd4c986..bacde7bc 100644
--- a/src/vm/opcodes.h
+++ b/src/vm/opcodes.h
@@ -54,6 +54,7 @@ OPCODE(RETURN)
OPCODE(EMPTY)
OPCODE(CLASS)
OPCODE(SUBCLASS)
+OPCODE(DEFINE_CLASS_ANNOTATIONS)
OPCODE(END_CLASS)
OPCODE(METHOD)
OPCODE(ENUM)
diff --git a/src/vm/scanner.c b/src/vm/scanner.c
index a70bc9c2..90db8691 100644
--- a/src/vm/scanner.c
+++ b/src/vm/scanner.c
@@ -359,6 +359,8 @@ Token scanToken(Scanner *scanner) {
if (isDigit(c)) return hexNumber(scanner);
switch (c) {
+ case '@':
+ return makeToken(scanner, TOKEN_AT);
case '(':
return makeToken(scanner, TOKEN_LEFT_PAREN);
case ')':
diff --git a/src/vm/scanner.h b/src/vm/scanner.h
index 33a7769f..206d7e7c 100644
--- a/src/vm/scanner.h
+++ b/src/vm/scanner.h
@@ -15,7 +15,7 @@ typedef enum {
TOKEN_SEMICOLON, TOKEN_COLON, TOKEN_SLASH,
TOKEN_STAR, TOKEN_STAR_STAR,
- TOKEN_PERCENT,
+ TOKEN_PERCENT, TOKEN_AT,
// Bitwise
TOKEN_AMPERSAND, TOKEN_CARET, TOKEN_PIPE,
diff --git a/src/vm/value.c b/src/vm/value.c
index 039f17ab..a43ab0c9 100644
--- a/src/vm/value.c
+++ b/src/vm/value.c
@@ -455,6 +455,12 @@ void printValue(Value value) {
free(output);
}
+void printValueError(Value value) {
+ char *output = valueToString(value);
+ fprintf(stderr, "%s", output);
+ free(output);
+}
+
static bool listComparison(Value a, Value b) {
ObjList *list = AS_LIST(a);
ObjList *listB = AS_LIST(b);
diff --git a/src/vm/value.h b/src/vm/value.h
index 943c46de..d4d8a736 100644
--- a/src/vm/value.h
+++ b/src/vm/value.h
@@ -107,4 +107,6 @@ char *valueTypeToString(DictuVM *vm, Value value, int *length);
void printValue(Value value);
+void printValueError(Value value);
+
#endif
diff --git a/src/vm/vm.c b/src/vm/vm.c
index d942782d..561ece68 100644
--- a/src/vm/vm.c
+++ b/src/vm/vm.c
@@ -861,12 +861,13 @@ static DictuInterpretResult run(DictuVM *vm) {
DISPATCH();
CASE_CODE(POP_REPL): {
- Value v = pop(vm);
+ Value v = peek(vm, 0);
if (!IS_NIL(v)) {
setReplVar(vm, v);
printValue(v);
printf("\n");
}
+ pop(vm);
DISPATCH();
}
@@ -1039,6 +1040,12 @@ static DictuInterpretResult run(DictuVM *vm) {
klass = klass->superclass;
}
+ if (strcmp(name->chars, "annotations") == 0) {
+ pop(vm); // Klass
+ push(vm, klassStore->annotations == NULL ? NIL_VAL : OBJ_VAL(klassStore->annotations));
+ DISPATCH();
+ }
+
RUNTIME_ERROR("'%s' class has no property: '%s'.", klassStore->name->chars, name->chars);
}
@@ -1966,6 +1973,15 @@ static DictuInterpretResult run(DictuVM *vm) {
DISPATCH();
}
+ CASE_CODE(DEFINE_CLASS_ANNOTATIONS): {
+ ObjDict *dict = AS_DICT(READ_CONSTANT());
+ ObjClass *klass = AS_CLASS(peek(vm, 0));
+
+ klass->annotations = dict;
+
+ DISPATCH();
+ }
+
CASE_CODE(END_CLASS): {
ObjClass *klass = AS_CLASS(peek(vm, 0));
diff --git a/tests/classes/annotations.du b/tests/classes/annotations.du
new file mode 100644
index 00000000..dcc32f47
--- /dev/null
+++ b/tests/classes/annotations.du
@@ -0,0 +1,29 @@
+/**
+ * annotations.du
+ *
+ * Testing class annotations
+ */
+
+class NoAnnotations {}
+
+assert(NoAnnotations.annotations == nil);
+
+@EmptyAnnotation
+@TrueAnnotation(true)
+@FalseAnnotation(false)
+@NumberAnnotation(10)
+@DecimalNumberAnnotation(10.5)
+@NilAnnotation(nil)
+class Test {
+
+}
+
+assert(Test.annotations.len() == 6);
+assert(Test.annotations['EmptyAnnotation'] == nil);
+assert(Test.annotations['TrueAnnotation'] == true);
+assert(Test.annotations['FalseAnnotation'] == false);
+assert(Test.annotations['NumberAnnotation'] == 10);
+assert(Test.annotations['DecimalNumberAnnotation'] == 10.5);
+assert(Test.annotations['NilAnnotation'] == nil);
+
+
diff --git a/tests/classes/getAttributes.du b/tests/classes/getAttributes.du
new file mode 100644
index 00000000..d7b081a4
--- /dev/null
+++ b/tests/classes/getAttributes.du
@@ -0,0 +1,48 @@
+/**
+ * getAttributes.du
+ *
+ * Testing the obj.getAttribute() method
+ *
+ */
+
+class Test {
+ var x = 10;
+
+ init(private priv = 10, var pub = 20) {
+ this.prop = 30;
+ }
+
+ private privMethod() {}
+
+ publicMethod() {}
+}
+
+assert(Test().getAttribute("x") == 10);
+assert(Test().getAttribute("x", nil) == 10);
+assert(Test().getAttribute("priv") == nil);
+assert(Test().getAttribute("priv", 10) == 10);
+assert(Test().getAttribute("privMethod") == nil);
+assert(Test().getAttribute("privMethod", 10) == 10);
+assert(Test().getAttribute("pub") == 20);
+assert(Test().getAttribute("pub", nil) == 20);
+assert(Test().getAttribute("prop") == 30);
+assert(Test().getAttribute("prop", nil) == 30);
+assert(type(Test().getAttribute("publicMethod") == "function"));
+assert(type(Test().getAttribute("publicMethod", nil) == "function"));
+
+class Inherit < Test {
+
+}
+
+assert(Inherit().getAttribute("x") == 10);
+assert(Inherit().getAttribute("x", nil) == 10);
+assert(Inherit().getAttribute("priv") == nil);
+assert(Inherit().getAttribute("priv", 10) == 10);
+assert(Inherit().getAttribute("privMethod") == nil);
+assert(Inherit().getAttribute("privMethod", 10) == 10);
+assert(Inherit().getAttribute("pub") == 20);
+assert(Inherit().getAttribute("pub", nil) == 20);
+assert(Inherit().getAttribute("prop") == 30);
+assert(Inherit().getAttribute("prop", nil) == 30);
+assert(type(Inherit().getAttribute("publicMethod") == "function"));
+assert(type(Inherit().getAttribute("publicMethod", nil) == "function"));
\ No newline at end of file
diff --git a/tests/classes/import.du b/tests/classes/import.du
index 7149b6ab..331911b4 100644
--- a/tests/classes/import.du
+++ b/tests/classes/import.du
@@ -17,4 +17,6 @@ import "parameters.du";
import "isInstance.du";
import "constructor.du";
import "optionalChaining.du";
-import "private.du";
\ No newline at end of file
+import "private.du";
+import "annotations.du";
+import "getAttributes.du";
\ No newline at end of file
diff --git a/tests/maths/maths.du b/tests/maths/maths.du
index d99b448a..e8fcf388 100644
--- a/tests/maths/maths.du
+++ b/tests/maths/maths.du
@@ -34,6 +34,10 @@ assert(Math.sin(1) < 0.845);
assert(Math.cos(0) == 1);
assert(Math.tan(1) > 1.5);
assert(Math.tan(1) < 1.6);
+assert(Math.gcd(32, 24, 12) == 4);
+assert(Math.gcd([32, 24, 12]) == 4);
+assert(Math.lcm(32, 24, 12) == 96);
+assert(Math.lcm([32, 24, 12]) == 96);
assert(Math.PI == 3.14159265358979);
assert(Math.e == 2.71828182845905);
\ No newline at end of file
diff --git a/tests/path/import.du b/tests/path/import.du
index 8906e8f9..d58d0bc8 100644
--- a/tests/path/import.du
+++ b/tests/path/import.du
@@ -11,4 +11,5 @@ import "isAbsolute.du";
import "realpath.du";
import "exists.du";
import "isDir.du";
-import "listDir.du";
\ No newline at end of file
+import "join.du";
+import "listDir.du";
diff --git a/tests/path/join.du b/tests/path/join.du
new file mode 100644
index 00000000..1d3d5a6b
--- /dev/null
+++ b/tests/path/join.du
@@ -0,0 +1,53 @@
+/**
+ * join.du
+ *
+ * Testing Path.join()
+ *
+ * Joins the path component strings using the directory separator.
+ */
+import Path;
+
+var result;
+var expected1 = '/tmp' + Path.dirSeparator + 'abcd' + Path.dirSeparator + 'efg';
+var expected2 = '/tmp' + Path.dirSeparator + 'abcd' + Path.dirSeparator + 'efg' + Path.dirSeparator;
+var manyDirSeps = Path.dirSeparator + Path.dirSeparator + Path.dirSeparator + Path.dirSeparator + Path.dirSeparator;
+
+// Test basic join with varargs
+result = Path.join('/tmp', 'abcd', 'efg');
+assert(result == expected1);
+
+// Test basic join with list
+result = Path.join(['/tmp', 'abcd', 'efg' + Path.dirSeparator]);
+assert(result == expected2);
+
+// Test join with trailing directory separators with varargs
+result = Path.join('/tmp' + Path.dirSeparator, 'abcd' + Path.dirSeparator, 'efg');
+assert(result == expected1);
+
+// Test join with trailing directory separators with list
+result = Path.join(['/tmp' + Path.dirSeparator, 'abcd' + Path.dirSeparator, 'efg' + Path.dirSeparator]);
+assert(result == expected2);
+
+// Test join with leading and trailing directory separators with varargs
+result = Path.join('/tmp' + Path.dirSeparator, Path.dirSeparator + 'abcd' + Path.dirSeparator, Path.dirSeparator + 'efg');
+assert(result == expected1);
+
+// Test join with leading and trailing directory separators with list
+result = Path.join(['/tmp' + Path.dirSeparator, Path.dirSeparator + 'abcd' + Path.dirSeparator, Path.dirSeparator + 'efg' + Path.dirSeparator]);
+assert(result == expected2);
+
+// Test join with many leading and trailing directory separators with varargs
+result = Path.join('/tmp' + manyDirSeps, manyDirSeps + 'abcd' + manyDirSeps, manyDirSeps + 'efg');
+assert(result == expected1);
+
+// Test join with many leading and trailing directory separators with list
+result = Path.join(['/tmp' + manyDirSeps, manyDirSeps + 'abcd' + manyDirSeps, manyDirSeps + 'efg' + manyDirSeps]);
+assert(result == expected2);
+
+// Test join with directory separators inside of part with varargs
+result = Path.join('/tmp', 'abcd' + Path.dirSeparator + 'efg');
+assert(result == expected1);
+
+// Test join with directory separators inside of part with list
+result = Path.join(['/tmp', 'abcd' + Path.dirSeparator + 'efg' + Path.dirSeparator]);
+assert(result == expected2);
diff --git a/tests/strings/title.du b/tests/strings/title.du
new file mode 100644
index 00000000..22a78c58
--- /dev/null
+++ b/tests/strings/title.du
@@ -0,0 +1,15 @@
+/**
+ * title.du
+ *
+ * Testing the str.title() method
+ *
+ * .title() method returns a string with first letter of each word capitalized; a title cased string.
+ */
+
+assert("title".title() == "Title");
+assert("dictu language".title() == "Dictu Language");
+assert("DiCtU".title() == "Dictu");
+assert("12345".title() == "12345");
+assert("12Dictu45".title() == "12dictu45");
+assert("!@£$%^&*".title() == "!@£$%^&*");
+assert("once upon a time".title()== "Once Upon A Time");
\ No newline at end of file