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