diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index b46f230..8d85d62 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -10,10 +10,16 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install MinGW toolchain + shell: powershell + run: | + choco install mingw --yes --no-progress + echo "C:\tools\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Create CMake cache run: | - cmake -S . -B cmake-build-release -DCMAKE_BUILD_TYPE=Release -G "Unix Makefiles" - cmake -S . -B cmake-build-debug -DCMAKE_BUILD_TYPE=Debug -G "Unix Makefiles" + cmake -S . -B cmake-build-release -DCMAKE_BUILD_TYPE=Release -G "MinGW Makefiles" + cmake -S . -B cmake-build-debug -DCMAKE_BUILD_TYPE=Debug -G "MinGW Makefiles" - name: Build main target shell: bash @@ -27,14 +33,16 @@ jobs: - name: Run program working-directory: .\cmake-build-release + shell: bash run: | - .\ovum.exe --help + ./ovum.exe --help - name: Run tests working-directory: .\cmake-build-debug + shell: bash run: | - echo "Currently unable to run tests on Windows Latest MinGW. See https://gitmemories.com/cristianadam/HelloWorld/issues/12 and https://github.com/microsoft/vscode-cmake-tools/issues/2451" - % .\ovum_tests.exe + # ./ovum_tests.exe + echo "Tests are not run on Windows MinGW due to issues with the test runner" build-matrix: name: Tests and application run on ${{ matrix.config.name }} @@ -123,3 +131,198 @@ jobs: working-directory: ./cmake-build/tests run: | valgrind --leak-check=full --track-origins=yes --error-exitcode=1 ./ovum_tests + + style-check: + name: Code style check with clang-format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install clang-format + run: | + sudo apt-get update && sudo apt-get -y install clang-format + + - name: Check code style + shell: bash + run: | + mapfile -t files < <(git ls-files '*.c' '*.cpp' '*.h' '*.hpp') + + if [ "${#files[@]}" -eq 0 ]; then + echo "No C/C++ files to check." + exit 0 + fi + + clang-format --dry-run --Werror "${files[@]}" 2>format_output.txt || { + cat format_output.txt + exit 1 + } + + - name: Comment on style issues + if: failure() && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const { execSync } = require('child_process'); + + try { + // Get list of files that need formatting + const rawFiles = execSync('git ls-files "*.c" "*.cpp" "*.h" "*.hpp"', { encoding: 'utf8' }).trim(); + + if (!rawFiles) { + console.log('No files require formatting checks.'); + return; + } + + const files = rawFiles.split('\n'); + + let comment = '## 🎨 Code Style Issues Found\n\n'; + comment += 'The following files have formatting issues:\n\n'; + let hasIssues = false; + + for (const file of files) { + try { + const result = execSync(`clang-format --dry-run --Werror "${file}" 2>&1`, { encoding: 'utf8' }); + } catch (error) { + comment += `- \`${file}\`: Formatting issues detected\n`; + hasIssues = true; + } + } + + if (!hasIssues) { + comment += 'No files with formatting issues were detected.'; + } else { + comment += '\nPlease run `clang-format -i ` to fix formatting issues.'; + } + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); + } catch (error) { + console.log('Could not create comment:', error.message); + } + + code-quality-check: + name: Code quality check with clang-tidy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install clang-tidy + run: | + sudo apt-get update && sudo apt-get -y install clang-tidy + + - name: Create CMake cache + run: | + cmake -S . -B cmake-build-tidy -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + + - name: Run clang-tidy + shell: bash + run: | + mapfile -t files < <(git ls-files '*.c' '*.cpp') + + if [ "${#files[@]}" -eq 0 ]; then + echo "No C/C++ files to analyze." + echo "" > tidy_output.txt + exit 0 + fi + + echo "Running clang-tidy on ${#files[@]} files..." + clang-tidy "${files[@]}" -p cmake-build-tidy --format-style=file > tidy_output.txt 2>&1 || true + + # Ensure file exists and is readable + if [ ! -f tidy_output.txt ]; then + echo "" > tidy_output.txt + fi + + - name: Count warnings and errors + id: count_issues + run: | + # Count errors and warnings - handle empty file case + if [ ! -s tidy_output.txt ]; then + errors=0 + warnings=0 + else + errors=$(grep -c "error:" tidy_output.txt 2>/dev/null || echo "0") + warnings=$(grep -c "warning:" tidy_output.txt 2>/dev/null || echo "0") + fi + + # Ensure we have clean integer values + errors=$(echo "$errors" | tr -d '\n' | head -c 10) + warnings=$(echo "$warnings" | tr -d '\n' | head -c 10) + + # Default to 0 if empty or non-numeric + errors=${errors:-0} + warnings=${warnings:-0} + + echo "errors=$errors" >> $GITHUB_OUTPUT + echo "warnings=$warnings" >> $GITHUB_OUTPUT + + echo "Found $errors errors and $warnings warnings" + + # Fail if more than 3 warnings or any errors + if [ "$errors" -gt 0 ] || [ "$warnings" -gt 3 ]; then + echo "clang-tidy found $errors errors and $warnings warnings" + cat tidy_output.txt + exit 1 + fi + + - name: Comment on quality issues + if: failure() && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + try { + let comment = '## 🔍 Code Quality Issues Found\n\n'; + + if (fs.existsSync('tidy_output.txt')) { + const output = fs.readFileSync('tidy_output.txt', 'utf8'); + const lines = output.split('\n'); + + let currentFile = ''; + let hasIssues = false; + + for (const line of lines) { + if (line.includes('error:') || line.includes('warning:')) { + const parts = line.split(':'); + if (parts.length >= 4) { + const file = parts[0]; + const lineNum = parts[1]; + const message = parts.slice(3).join(':').trim(); + + if (file !== currentFile) { + if (hasIssues) comment += '\n'; + comment += `### \`${file}\`\n\n`; + currentFile = file; + hasIssues = true; + } + + const issueType = line.includes('error:') ? '❌ Error' : '⚠️ Warning'; + comment += `- **Line ${lineNum}**: ${issueType} - ${message}\n`; + } + } + } + + if (!hasIssues) { + comment += 'No specific issues found in the output.'; + } + } else { + comment += 'Could not read clang-tidy output.'; + } + + comment += '\n\nPlease review and fix the issues above.'; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); + } catch (error) { + console.log('Could not create comment:', error.message); + } diff --git a/README.md b/README.md index 7521dbe..fa41408 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,15 @@ Ovum is a strongly statically typed, single-threaded language focused on safety, --- -## [Types and Nullability](docs/reference/types.md) +## [Types](docs/reference/types.md) and [Nullability](docs/reference/nullable.md) -- Nullable types: append `?` (e.g., `Int?`). -- Safe call `?.`, Elvis `?:`, non‑null `!!`. -- Type test `is`, cast `as` (downcast yields nullable type). -- Explicit cast to `Bool` allowed for any value. +- **Fundamental types**: `int`, `float`, `byte`, `char`, `bool`, `pointer` (value types, not nullable, not Objects) +- **Primitive reference types**: `Int`, `Float`, `Byte`, `Char`, `Bool`, `Pointer` (reference wrappers, nullable, Objects) +- **Implicit conversion**: Literals convert to primitives (`val count: Int = 0`) +- **Nullable types**: Append `?` to reference types only (e.g., `Int?`, `String?`) +- **Safe call `?.`**, **Elvis `?:`** for null handling +- **Type test `is`**, **cast `as`** (downcast yields nullable type) +- **Copy assignment `:=`** for deep copying reference types --- @@ -51,9 +54,9 @@ Ovum is a strongly statically typed, single-threaded language focused on safety, - Arithmetic: `+ - * / %` - Comparison: `== != < <= > >=` - Boolean: `&& || ! xor` (short‑circuit `&&`/`||`). -- Assignment: `=` +- Assignment: `=` (reference assignment), `:=` (copy assignment) - Member/calls: `. ()` and safe `?.` -- Null handling: `?. ?: !!` +- Null handling: `?. ?:` - Type ops: `as`, `is` - Namespace: `::` @@ -77,7 +80,7 @@ Ovum is a strongly statically typed, single-threaded language focused on safety, - Pipeline: `.ovum` → bytecode → Ovum VM. - GC for memory safety; JIT compiles hot paths. - Single‑threaded execution model. -- Architectures: amd64, arm64. Numeric widths: `Int` 8 bytes, `Float` 8 bytes. +- Architectures: amd64, arm64. Numeric widths: `int` 8 bytes, `float` 8 bytes. - Entry point: `Main(args: StringArray): Int`. Build & Run (conceptual): write `.ovum`, compile (parse, type‑check, enforce const/pure), run on VM (JIT + GC). @@ -98,7 +101,7 @@ Build & Run (conceptual): write `.ovum`, compile (parse, type‑check, enforce c Only inside `unsafe { ... }`: - Global `var` and `static var` writes. -- Const/mutable casts; `Pointer`, address‑of, dereference. +- Const/mutable casts; `pointer`, address‑of, dereference. - Manual destructor calls. - `sys::Interope`; casting any value to (const or mutable) `ByteArray`. @@ -111,35 +114,40 @@ Only inside `unsafe { ... }`: ```ovum // .ovum file fun Main(args: StringArray): Int { - val count: Int = args.Length() + val count: Int = args.Length() // Built-in returns Int sys::Print("Args count: " + count.ToString()) - return 0 + return 0 // Implicit conversion from literal } ``` ### Pure functions with caching ```ovum -pure fun Fib(n: Int): Int { +pure fun Fib(n: int): int { if (n <= 1) return n - return Fib(n - 1) + Fib(n - 2) + val fib1: int = Fib(n - 1) + val fib2: int = Fib(n - 2) + return fib1 + fib2 } ``` -### `is`, `as`, `!!` and ByteArray casts +### `is`, `as` and ByteArray casts ```ovum fun DemoCasts(obj: Object): Void { if (obj is Point) { - val p: Point = (obj as Point)!! // nullable cast + assert - sys::Print(p.ToString()) + val p: Point? = obj as Point + if (p != null) { + val nonNullP: Point = p ?: Point(0, 0) // Use Elvis operator + sys::Print(nonNullP.ToString()) + } } // Bool cast - val b1: Bool = (0 as Bool) // false - val b2: Bool = (42 as Bool) // true - val b3: Bool = (obj as Bool) // always true - val b4: Bool = ((obj as Point) as Bool) // true if obj is a Point + val b1: Bool = 0 as bool // false + val b2: Bool = 42 as bool // true + val b3: Bool = obj as bool // always true + val b4: Bool = (obj as Point) as bool // true if obj is a Point // Unsafe: raw byte views unsafe { @@ -170,11 +178,14 @@ class DefinedFunctional { } val AddNullable: CustomFunctional = pure fun(a: Int?, b: Int?): Int { - return (a ?: 0) + (b ?: 0) + val aVal: int = a ?: 0 // Implicit conversion from Int? to int + val bVal: int = b ?: 0 + return Int(aVal + bVal) // Implicit conversion from int to Int } fun Main(args: StringArray): Int { - return AddNullable(2, DefinedFunctional(-1)(2)) + // Constructor call then functional call via `call` + return AddNullable(2, DefinedFunctional(-1)(2)) // Implicit conversion from literals } ``` @@ -198,29 +209,30 @@ class Multiplier implements ICalculator { } } -pure fun ProcessNumbers(calc: ICalculator, numbers: IntArray): Int { - var result: Int = 0 +pure fun ProcessNumbers(calc: ICalculator, numbers: IntArray): int { + var result: int = 0 for (num in numbers) { - result = result + calc.Calculate(num, 2) + val calcResult: Int = calc.Calculate(num, 2) // Implicit conversion from literal + result = result + calcResult // Implicit conversion } return result } -fun Main(args: StringArray): Int { +fun Main(args: StringArray): int { val numbers: IntArray = IntArray(3) - numbers[0] = 5 - numbers[1] = 10 - numbers[2] = 15 + numbers[0] := 5 // Implicit conversion from literal + numbers[1] := 10 + numbers[2] := 15 val adder: ICalculator = Adder() val multiplier: ICalculator = Multiplier() - val sumResult: Int = ProcessNumbers(adder, numbers) - val productResult: Int = ProcessNumbers(multiplier, numbers) + val sumResult: int = ProcessNumbers(adder, numbers) + val productResult: int = ProcessNumbers(multiplier, numbers) - sys::Print("Sum result: " + sumResult.ToString()) - sys::Print("Product result: " + productResult.ToString()) + sys::Print("Sum result: " + Int(sumResult).ToString()) + sys::Print("Product result: " + Int(productResult).ToString()) - return 0 + return 0 // Implicit conversion from literal } ``` diff --git a/docs/reference/builtin_types.md b/docs/reference/builtin_types.md index 6565b19..2ace559 100644 --- a/docs/reference/builtin_types.md +++ b/docs/reference/builtin_types.md @@ -36,13 +36,12 @@ val hash: Int = greeting.GetHash() Any primitive type can be made nullable by appending `?` (e.g., `Int?`, `String?`). Nullable types are passed by reference and can hold either a value or `null`. -**Important**: You cannot directly call methods on nullable types using `.` - you must use the safe call operator `?.` or non-null assertion `!!`. +**Important**: You cannot directly call methods on nullable types using `.` - you must use the safe call operator `?.`. ```ovum val nullableString: String? = "Hello" // val length: Int = nullableString.Length() // ERROR: Cannot call method directly on nullable val safeLength: Int = nullableString?.Length() ?: 0 // Correct: Use safe call -val forcedLength: Int = nullableString!!.Length() // Correct: Use non-null assertion ``` ## Array Types diff --git a/docs/reference/code_examples.md b/docs/reference/code_examples.md index 711160e..2400276 100644 --- a/docs/reference/code_examples.md +++ b/docs/reference/code_examples.md @@ -7,7 +7,7 @@ Here are some code examples to help you get started with Ovum. ```ovum // .ovum file fun Main(args: StringArray): Int { - val count: Int = args.Length() + val count: Int = args.Length() // Built-in returns Int sys::Print("Args count: " + count.ToString()) return 0 } @@ -20,14 +20,14 @@ fun DemoNulls(): Void { val a: Int? = null val b: Int? = 5 - val sum: Int = (a ?: 0) + (b ?: 0) // Elvis - sys::Print("Sum = " + sum.ToString()) + val aVal: int = a ?: 0 + val bVal: int = b ?: 0 + val sum: int = aVal + bVal + sys::Print("Sum = " + Int(sum).ToString()) val name: String? = null - sys::Print("Name length = " + (name?.Length() ?: 0).ToString()) - - val mustNotBeNull: Int = (b!!) // ok - // val crash: Int = (a!!) // aborts (unhandleable) + val length: int = (name?.Length() ?: 0) as int // Built-in returns Int + sys::Print("Name length = " + Int(length).ToString()) } ``` @@ -67,24 +67,29 @@ interface IComparable { fun IsLess(other: Object): Bool } interface IHashable { fun GetHash(): Int } class Point implements IStringConvertible, IComparable, IHashable { - public val X: Int - public val Y: Int + public val X: int + public val Y: int - public fun Point(x: Int, y: Int): Point { this.X = x; this.Y = y; return this; } + public fun Point(x: int, y: int): Point { this.X = x; this.Y = y; return this; } public override fun ToString(): String { - return "(" + X.ToString() + ", " + Y.ToString() + ")" + return "(" + Int(X).ToString() + ", " + Int(Y).ToString() + ")" } public override fun IsLess(other: Object): Bool { if (!(other is Point)) return false - val p: Point = (other as Point)!! // safe after is + !! - if (this.X != p.X) return this.X < p.X - return this.Y < p.Y + val p: Point? = other as Point + if (p != null) { + val nonNullP: Point = p ?: Point(0, 0) // Use Elvis operator + if (this.X != nonNullP.X) return this.X < nonNullP.X + return this.Y < nonNullP.Y + } + return false } public override fun GetHash(): Int { - return (X * 1315423911) ^ (Y * 2654435761) + val hash: int = (X * 1315423911) ^ (Y * 2654435761) + return Int(hash) } } ``` @@ -92,27 +97,32 @@ class Point implements IStringConvertible, IComparable, IHashable { ## 5) Pure Functions with Caching ```ovum -pure fun Fib(n: Int): Int { +pure fun Fib(n: int): int { if (n <= 1) return n - return Fib(n - 1) + Fib(n - 2) + val fib1: int = Fib(n - 1) + val fib2: int = Fib(n - 2) + return fib1 + fib2 } // For user-defined reference types as parameters, implement IComparable. ``` -## 6) `is`, `as`, `!!`, and ByteArray Casts +## 6) `is`, `as`, and ByteArray Casts ```ovum fun DemoCasts(obj: Object): Void { if (obj is Point) { - val p: Point = (obj as Point)!! // nullable cast + assert - sys::Print(p.ToString()) + val p: Point? = obj as Point + if (p != null) { + val nonNullP: Point = p ?: Point(0, 0) // Use Elvis operator + sys::Print(nonNullP.ToString()) + } } - // Bool cast - val b1: Bool = (0 as Bool) // false - val b2: Bool = (42 as Bool) // true - val b3: Bool = (obj as Bool) // always true - val b4: Bool = ((obj as Point) as Bool) // true if obj is a Point + // bool cast + val b1: Bool = 0 as bool // false + val b2: Bool = 42 as bool // true + val b3: Bool = obj as bool // always true + val b4: Bool = (obj as Point) as bool // true if obj is a Point // Unsafe: raw byte views unsafe { @@ -155,7 +165,7 @@ fun Main(args: StringArray): Int { ```ovum fun DemoControlFlow(): Void { - var i: Int = 0 + var i: int = 0 // While loop with break and continue while (i < 10) { @@ -166,15 +176,15 @@ fun DemoControlFlow(): Void { if (i == 7) { break // Exit loop } - sys::Print("i = " + i.ToString()) + sys::Print("i = " + Int(i).ToString()) i = i + 1 } // For loop over array val numbers: IntArray = IntArray(3) - numbers[0] = 10 - numbers[1] = 20 - numbers[2] = 30 + numbers[0] := 10 + numbers[1] := 20 + numbers[2] := 30 for (num in numbers) { sys::Print("Number: " + num.ToString()) @@ -189,13 +199,13 @@ fun DemoUnsafeOperations(): Void { // Unsafe block for low-level operations unsafe { // Global mutable state (unsafe) - static var globalCounter: Int = 0 + static var globalCounter: int = 0 globalCounter = globalCounter + 1 // Pointer operations (unsafe) val obj: Point = Point(10, 20) - val ptr: Pointer = &obj // address-of - val deref: Object = *ptr // dereference to Object, Pointer is not typed + val ptr: pointer = &obj // address-of + val deref: Object = *ptr // dereference to Object, pointer is not typed // ByteArray casting (unsafe) val bytes: ByteArray = (obj as ByteArray) @@ -203,8 +213,8 @@ fun DemoUnsafeOperations(): Void { // Foreign function interface (unsafe) val input: ByteArray = "Hello".ToUtf8Bytes() - val output: ByteArray = ByteArray(4) - val result: Int = sys::Interope("libc.so", "strlen", input, output) + val output: ByteArray = ByteArray(8) + val result: int = sys::Interope("libc.so", "strlen", input, output) } } ``` @@ -230,8 +240,11 @@ class User { fun ProcessUsers(users: UserList): Void { for (i in 0..users.Length()) { - val user: User = (users[i] as User)!! - sys::Print("User " + user.Id.ToString() + ": " + user.Name) + val user: User? = users[i] as User + if (user != null) { + val nonNullUser: User = user ?: User(0, "Unknown") // Use Elvis operator + sys::Print("User " + nonNullUser.Id.ToString() + ": " + nonNullUser.Name) + } } } ``` @@ -241,10 +254,10 @@ fun ProcessUsers(users: UserList): Void { ```ovum class DatabaseConnection { - private val ConnectionId: Int - private val IsConnected: Bool + private val ConnectionId: int + private val IsConnected: bool - public fun DatabaseConnection(id: Int): DatabaseConnection { + public fun DatabaseConnection(id: int): DatabaseConnection { this.ConnectionId = id this.IsConnected = true // Establish database connection @@ -261,7 +274,7 @@ class DatabaseConnection { public destructor(): Void { if (IsConnected) { // Close database connection - sys::Print("Closing connection " + ConnectionId.ToString()) + sys::Print("Closing connection " + Int(ConnectionId).ToString()) } } } @@ -296,28 +309,29 @@ class Multiplier implements ICalculator { } } -pure fun ProcessNumbers(calc: ICalculator, numbers: IntArray): Int { - var result: Int = 0 +pure fun ProcessNumbers(calc: ICalculator, numbers: IntArray): int { + var result: int = 0 for (num in numbers) { - result = result + calc.Calculate(num, 2) + val calcResult: Int = calc.Calculate(num, 2) + result = result + calcResult } return result } -fun Main(args: StringArray): Int { +fun Main(args: StringArray): int { val numbers: IntArray = IntArray(3) - numbers[0] = 5 - numbers[1] = 10 - numbers[2] = 15 + numbers[0] := 5 + numbers[1] := 10 + numbers[2] := 15 val adder: ICalculator = Adder() val multiplier: ICalculator = Multiplier() - val sumResult: Int = ProcessNumbers(adder, numbers) - val productResult: Int = ProcessNumbers(multiplier, numbers) + val sumResult: int = ProcessNumbers(adder, numbers) + val productResult: int = ProcessNumbers(multiplier, numbers) - sys::Print("Sum result: " + sumResult.ToString()) - sys::Print("Product result: " + productResult.ToString()) + sys::Print("Sum result: " + Int(sumResult).ToString()) + sys::Print("Product result: " + Int(productResult).ToString()) return 0 } diff --git a/docs/reference/design.md b/docs/reference/design.md index 9086ba0..461ce30 100644 --- a/docs/reference/design.md +++ b/docs/reference/design.md @@ -16,7 +16,7 @@ Ovum's core design principles center around: ## Key Design Points * **Strong static typing** with **immutability by default** (`var` required for mutation). -* **Nullable types** and Kotlin-style null-handling: `Type?`, safe calls `?.`, Elvis `?:`, non-null assertion `!!`. +* **Nullable types** and Kotlin-style null-handling: `Type?`, safe calls `?.`, Elvis `?:`. * **Pure functions** (no side effects, VM-level result caching). * **Classes & interfaces** diff --git a/docs/reference/expressions_and_operators.md b/docs/reference/expressions_and_operators.md index 1c8043c..e56a42f 100644 --- a/docs/reference/expressions_and_operators.md +++ b/docs/reference/expressions_and_operators.md @@ -21,11 +21,12 @@ Expressions in Ovum include literal values, variable references, function calls, * `&&` (logical AND) - short-circuit evaluation * `||` (logical OR) - short-circuit evaluation * `!` (negation) - unary operator -* `xor` (exclusive OR) - infix operator on `Bool` +* `xor` (exclusive OR) - infix operator on `bool` -## Assignment Operator +## Assignment Operators -* `=` (assignment) - assigns a value to a mutable variable or field. The left-hand side must be a mutable variable or field. +* `=` (reference assignment) - assigns a reference to a mutable variable or field. The left-hand side must be a mutable variable or field. +* `:=` (copy assignment) - performs deep copy for reference types. Creates a new object with the same content as the source. ## Member Access @@ -37,13 +38,12 @@ Expressions in Ovum include literal values, variable references, function calls, ## Type Operations * `expr as Type` - explicit cast (downcast yields nullable type) -* `expr is Type` - type test (returns `Bool`) +* `expr is Type` - type test (returns `bool`) ## Null Handling * `expr?.member` - safe call (calls only if expr is not null) * `expr ?: default` - Elvis operator (returns expr if not null, otherwise default) -* `expr!!` - non-null assertion (throws error if expr is null) ## Namespace Resolution diff --git a/docs/reference/lexical_structure.md b/docs/reference/lexical_structure.md index a8c79bf..c207aee 100644 --- a/docs/reference/lexical_structure.md +++ b/docs/reference/lexical_structure.md @@ -24,8 +24,8 @@ Ovum reserves certain words like `fun`, `class`, `interface`, `var`, `override`, * **Arithmetic**: `+`, `-`, `*`, `/`, `%` * **Comparison**: `==`, `!=`, `<`, `<=`, `>`, `>=` * **Boolean logic**: `&&` (logical AND), `||` (logical OR), `!` (negation), `xor` (exclusive OR) -* **Assignment**: `=` -* **Null handling**: `?.` (safe call), `?:` (Elvis), `!!` (non-null assertion) +* **Assignment**: `=` (reference assignment), `:=` (copy assignment) +* **Null handling**: `?.` (safe call), `?:` (Elvis) * **Type operations**: `as` (cast), `is` (type test) * **Punctuation**: `,` (comma), `;` (semicolon), `:` (colon), `()` (parentheses), `{}` (braces), `[]` (brackets) * **Namespace resolution**: `::` @@ -87,14 +87,16 @@ TypeAliasDecl ::= "typealias" Identifier "=" Type ";" ; Type ::= NullableType | NonNullType ; NullableType ::= NonNullType "?" ; -NonNullType ::= PrimitiveType +NonNullType ::= FundamentalType + | PrimitiveRefType | "String" | "IntArray" | "FloatArray" | "BoolArray" | "CharArray" | "ByteArray" | "PointerArray" | "ObjectArray" | "StringArray" | Identifier ; // class/interface names (non-primitive) -PrimitiveType ::= "Int" | "Float" | "Bool" | "Char" | "Byte" | "Pointer" ; +FundamentalType ::= "int" | "float" | "bool" | "char" | "byte" | "pointer" ; +PrimitiveRefType ::= "Int" | "Float" | "Bool" | "Char" | "Byte" | "Pointer" ; Block ::= "{" { Statement } "}" ; Statement ::= VarDeclStmt | ExprStmt | ReturnStmt | IfStmt | WhileStmt | ForStmt | UnsafeStmt | Block ; @@ -108,7 +110,7 @@ ForStmt ::= "for" "(" Identifier "in" Expression ")" Statement ; UnsafeStmt ::= "unsafe" Block ; Expression ::= Assignment ; -Assignment ::= ElvisExpr [ "=" Assignment ] ; +Assignment ::= ElvisExpr [ ("=" | ":=") Assignment ] ; ElvisExpr ::= OrExpr [ "?:" ElvisExpr ] ; // right-assoc @@ -129,8 +131,7 @@ PostfixOp ::= "." Identifier | "." Identifier "(" [ ArgList ] ")" | "(" [ ArgList ] ")" // function call or callable object call | "as" Type // explicit cast; downcast yields nullable type - | "is" Type // type test → Bool - | "!!" // non-null assertion + | "is" Type // type test → bool | "?." Identifier [ "(" [ ArgList ] ")" ] // safe call chain | "?." "(" [ ArgList ] ")" // safe callable object call ; diff --git a/docs/reference/nullable.md b/docs/reference/nullable.md index da03e80..248ea24 100644 --- a/docs/reference/nullable.md +++ b/docs/reference/nullable.md @@ -4,23 +4,24 @@ This document describes how nullable types work in Ovum, including their restric ## Creating Nullable Types -Append `?` to make a type **nullable**: `Int?`, `String?`, `Point?`. Nullable types are passed by reference and can hold either a value or `null`. +Append `?` to make a **reference type** nullable: `Int?`, `String?`, `Point?`. **Fundamental types** (`int`, `float`, `bool`, `char`, `byte`, `pointer`) cannot be made nullable. ```ovum val nullableInt: Int? = null val nullableString: String? = "Hello" val nullablePoint: Point? = Point(10, 20) + +// val invalidNullable: int? = null // ERROR: Fundamental types cannot be nullable ``` ## Method Call Restrictions -**Important**: You cannot directly call methods on nullable types using `.` - you must use the safe call operator `?.` or non-null assertion `!!`. +**Important**: You cannot directly call methods on nullable types using `.` - you must use the safe call operator `?.`. ```ovum val nullableString: String? = "Hello" // val length: Int = nullableString.Length() // ERROR: Cannot call method directly on nullable val safeLength: Int = nullableString?.Length() ?: 0 // Correct: Use safe call -val forcedLength: Int = nullableString!!.Length() // Correct: Use non-null assertion ``` ## Null Handling Operators @@ -47,32 +48,26 @@ val nullableString: String? = null val result: String = nullableString ?: "default" // Uses "default" if nullableString is null ``` -### Non-null Assertion (`!!`) - -`x!!` throws an unhandleable error if `x == null`. Use with caution - only when you're certain the value is not null. - -```ovum -val nullableInt: Int? = 42 -val mustExist: Int = nullableInt!! // Safe - nullableInt is not null - -// val crash: Int = (null as Int?)!! // ERROR: Will abort the program -``` ## Type Casting ### Cast to Bool -Any value can be explicitly cast to `Bool`: +Any value can be explicitly cast to `bool`: -* **Primitives**: zero → `false`, non-zero → `true` -* **Non-primitives**: `true` iff the reference is a valid (non-null, live) object +* **Fundamentals, primitive reference types**: zero → `false`, non-zero → `true` +* **Non-primitive reference types and nullable primitives**: `true` iff the reference is a valid (non-null, live) object ```ovum val nullableInt: Int? = null -val isNull: Bool = (nullableInt as Bool) // false (null is falsy) +val isNull: bool = (nullableInt as bool) // false (null is falsy) -val someInt: Int? = 42 -val isNotNull: Bool = (someInt as Bool) // true (non-null is truthy) +val someInt: Int? = 42 // Implicit conversion from literal +val isNotNull: bool = (someInt as bool) // true (non-null is truthy) + +// Converting nullable primitives to fundamentals +val nullablePrimitive: Int? = 42 // Implicit conversion from literal +val fundamentalValue: int = (nullablePrimitive as Int) as int // Two-step conversion ``` ## Chaining Operations @@ -85,7 +80,9 @@ val nameLength: Int = person?.Name?.Length() ?: 0 // Equivalent to: val nameLength: Int = if (person != null && person.Name != null) { - person.Name.Length() + val nonNullPerson: Person = person ?: Person("Unknown") // Use Elvis operator + val nonNullName: String = nonNullPerson.Name ?: "Unknown" // Use Elvis operator + nonNullName.Length() } else { 0 } @@ -97,21 +94,18 @@ All nullable types support the same operators but cannot directly call methods: ```ovum val nullableString: String? = "Hello" -val nullableInt: Int? = 42 +val nullableInt: Int? = 42 // Implicit conversion from literal // Safe operations -val safeLength: Int = nullableString?.Length() ?: 0 +val safeLength: int = (nullableString?.Length() ?: 0) as int // Built-in returns Int val safeToString: String = nullableInt?.ToString() ?: "null" - -// Unsafe operations (will crash if null) -val forcedLength: Int = nullableString!!.Length() -val forcedToString: String = nullableInt!!.ToString() ``` ## Best Practices -1. **Prefer safe calls** over non-null assertions when possible -2. **Use Elvis operator** to provide sensible defaults -3. **Avoid non-null assertions** unless you're certain the value exists -4. **Chain operations** for cleaner null handling code -5. **Consider using `if` statements** for complex null checks instead of deeply nested safe calls +1. **Always use safe calls** (`?.`) for nullable types +2. **Use Elvis operator** (`?:`) to provide sensible defaults +3. **Chain operations** for cleaner null handling code +4. **Consider using `if` statements** for complex null checks instead of deeply nested safe calls +5. **Use copy assignment** (`:=`) when you need independent copies of nullable objects +6. **Convert to fundamentals** when you need value semantics: `(nullablePrimitive as PrimitiveType) as fundamentalType` diff --git a/docs/reference/object_model.md b/docs/reference/object_model.md index 75fa1f9..19a175b 100644 --- a/docs/reference/object_model.md +++ b/docs/reference/object_model.md @@ -68,7 +68,7 @@ class Point implements IComparable { public override fun IsLess(other: Object): Bool { if (!(other is Point)) return false - val p: Point = (other as Point)!! + val p: Point = (other as Point) ?: Point(0, 0) if (this.X != p.X) return this.X < p.X return this.Y < p.Y } @@ -289,13 +289,13 @@ val comparable: IComparable = point // Upcast to interface // Downcasting with type test if (obj is Point) { - val p: Point = (obj as Point)!! // Safe after type test + val p: Point = (obj as Point) ?: Point(0, 0) sys::Print("Point: " + p.ToString()) } // Type test operator if (shape is ColoredRectangle) { - val rect: ColoredRectangle = (shape as ColoredRectangle)!! + val rect: ColoredRectangle = (shape as ColoredRectangle) ?: ColoredRectangle(0, 0, "red") sys::Print("Rectangle color: " + rect.GetColor()) } ``` @@ -383,7 +383,7 @@ class ResourceManager { **Type Safety:** - Use type tests before casting (`is` before `as`) -- Prefer safe operations (`?.` and `?:` over `!!`) +- Prefer safe operations - Handle nullable types properly ```ovum @@ -395,7 +395,7 @@ interface IWritable { fun Write(content: String): Void } fun SafeProcessObject(obj: Object?): Void { val result: String = obj?.ToString() ?: "null" if (obj is Person) { - val person: Person = (obj as Person)!! + val person: Person = (obj as Person) ?: Person("Unknown") sys::Print("Person: " + person.ToString()) } } diff --git a/docs/reference/syntax.md b/docs/reference/syntax.md index f5a7611..d0a987e 100644 --- a/docs/reference/syntax.md +++ b/docs/reference/syntax.md @@ -74,11 +74,13 @@ class DefinedFunctional { } val AddNullable: CustomFunctional = pure fun(a: Int?, b: Int?): Int { - return (a ?: 0) + (b ?: 0) + val aVal: int = a ?: 0 // Conversion from Int? to int + val bVal: int = b ?: 0 + return aVal + bVal } fun Main(args: StringArray): Int { // Constructor call then functional call via `call` - return AddNullable(2, DefinedFunctional(-1)(2)) + return AddNullable(2, DefinedFunctional(-1)(2)) // Implicit conversion from literals } ``` diff --git a/docs/reference/types.md b/docs/reference/types.md index d23fe15..12e346c 100644 --- a/docs/reference/types.md +++ b/docs/reference/types.md @@ -2,34 +2,57 @@ Ovum has a rich type system with primitive types and user-defined types. The type system is static and does not permit implicit type coercions (an `Int` won't automatically become a `Float` without an explicit cast, for example). -## Primitive Types +## Fundamental Types + +Fundamental types are passed by value and represent the basic building blocks of the language: ### Numeric Types -* **`Int`** (8 bytes) - 64-bit signed integer +* **`int`** (8 bytes) - 64-bit signed integer * Literals: `42`, `-17`, `0x1A` (hex), `0b1010` (binary) -* **`Float`** (8 bytes) - 64-bit floating-point number (IEEE 754 double precision) +* **`float`** (8 bytes) - 64-bit floating-point number (IEEE 754 double precision) * Literals: `3.14`, `2.0e10`, `1.5E-3`, `.5`, `5.` * Special values: `Infinity`, `-Infinity`, `NaN` -* **`Byte`** (1 byte) - 8-bit unsigned integer +* **`byte`** (1 byte) - 8-bit unsigned integer * Literals: `255`, `0x00`, `0b11111111` ### Character and Boolean Types -* **`Char`** - single Unicode character (UTF-32) +* **`char`** - single Unicode character (UTF-32) * Literals: `'A'`, `'中'`, `'\n'`, `'\t'`, `'\0'` -* **`Bool`** - Boolean value (`true`, `false`) - * Any value can be explicitly cast to `Bool` +* **`bool`** - Boolean value (`true`, `false`) + * Any value can be explicitly cast to `bool` ### Low-Level Types -* **`Pointer`** - raw memory address *(only meaningful in `unsafe` code)* +* **`pointer`** - raw memory address *(only meaningful in `unsafe` code)* * Used for FFI and low-level memory operations -> **Nullable Primitives**: Any primitive type can be made nullable by appending `?` (e.g., `Int?`, `Float?`, `Bool?`). Nullable primitives are reference types. +> **Fundamental Type Constraints**: Fundamental types cannot be made nullable (`int?` is invalid). They are not `Object` types and cannot be stored in `ObjectArray` or cast to `Object`. To convert nullable primitives to fundamentals, cast to primitive first: `(nullableInt as Int) as int`. + +## Primitive Reference Types + +Primitive reference types are built-in reference wrappers around fundamental types, passed by reference: + +### Numeric Reference Types + +* **`Int`** - reference wrapper for `int` values +* **`Float`** - reference wrapper for `float` values +* **`Byte`** - reference wrapper for `byte` values + +### Character and Boolean Reference Types + +* **`Char`** - reference wrapper for `char` values +* **`Bool`** - reference wrapper for `bool` values + +### Low-Level Reference Types + +* **`Pointer`** - reference wrapper for `pointer` values *(only meaningful in `unsafe` code)* + +> **Nullable Primitives**: Any primitive reference type can be made nullable by appending `?` (e.g., `Int?`, `Float?`, `Bool?`). ## Reference Types @@ -48,12 +71,12 @@ Ovum has a rich type system with primitive types and user-defined types. The typ Ovum provides specialized array classes for different element types (no generics/templates): **Primitive Arrays:** -* `IntArray` - array of `Int` values -* `FloatArray` - array of `Float` values -* `BoolArray` - array of `Bool` values -* `CharArray` - array of `Char` values -* `ByteArray` - array of `Byte` values -* `PointerArray` - array of `Pointer` values +* `IntArray` - array of `Int` reference wrappers +* `FloatArray` - array of `Float` reference wrappers +* `BoolArray` - array of `Bool` reference wrappers +* `CharArray` - array of `Char` reference wrappers +* `ByteArray` - array of `Byte` reference wrappers +* `PointerArray` - array of `Pointer` reference wrappers **Object Arrays:** * `ObjectArray` - array of any `Object`-derived types @@ -61,7 +84,7 @@ Ovum provides specialized array classes for different element types (no generics **Array Creation:** ```ovum -val numbers: IntArray = IntArray(10) // Create array of size 10 +val numbers: IntArray = IntArray(10) // Create array of Int reference wrappers val names: StringArray = StringArray(5) // Create string array of size 5 val objects: ObjectArray = ObjectArray(3) // Create object array of size 3 ``` @@ -80,6 +103,26 @@ fun ProcessUser(id: UserId, name: UserName): Void { ``` +## Assignment Operators + +### Reference Assignment (`=`) + +The standard assignment operator assigns references for reference types: + +```ovum +val original: String = "Hello" +val reference: String = original // Both variables point to the same string +``` + +### Copy Assignment (`:=`) + +The copy assignment operator performs deep copy for reference types: + +```ovum +val original: String = "Hello" +val copy: String := original // Creates a new string with the same content +``` + ## Type Casting ### Explicit Casting @@ -87,30 +130,49 @@ fun ProcessUser(id: UserId, name: UserName): Void { Use the `as` operator for explicit casting: ```ovum -val intValue: Int = 42 -val floatValue: Float = (intValue as Float) // Int to Float -val stringValue: String = (intValue as String) // Int to String +val intValue: int = 42 +val floatValue: float = (intValue as float) // int to float -val floatNum: Float = 3.14 -val intNum: Int = (floatNum as Int) // Float to Int (truncates) +val floatNum: float = 3.14 +val intNum: int = (floatNum as int) // float to int (truncates) + +// Implicit bidirectional casting between fundamental and primitive reference types +val fundamentalInt: int = 42 +val primitiveInt: Int = fundamentalInt // Implicit: int -> Int +val backToFundamental: int = primitiveInt // Implicit: Int -> int + +// Implicit conversion from literals to primitive types +val count: Int = 0 // Implicit: int literal -> Int +val flag: Bool = true // Implicit: bool literal -> Bool +val pi: Float = 3.14 // Implicit: float literal -> Float + +// Arithmetic works seamlessly +val sum: Int = 10 + 20 // Int + Int = Int (implicit conversion from literals) +val result: int = sum + 5 // Int + int = int (implicit conversion) ``` ### Boolean Casting -Any value can be explicitly cast to `Bool`: +Any value can be explicitly cast to `bool`: ```ovum -val intVal: Int = 42 -val boolVal: Bool = (intVal as Bool) // true (non-zero) +val intVal: int = 42 +val boolVal: bool = (intVal as bool) // true (non-zero) -val zeroInt: Int = 0 -val falseBool: Bool = (zeroInt as Bool) // false (zero) +val zeroInt: int = 0 +val falseBool: bool = (zeroInt as bool) // false (zero) val nullString: String? = null -val nullBool: Bool = (nullString as Bool) // false (null) +val nullBool: bool = (nullString as bool) // false (null) + +// With primitive reference types (implicit conversion) +val primitiveInt: Int = 42 // Implicit conversion from literal +val primitiveBool: bool = primitiveInt // Implicit: Int -> bool +val boolRef: Bool = true // Implicit: bool literal -> Bool ``` -**Rules:** Primitives: zero → `false`, non-zero → `true`. References: `null` → `false`, non-null → `true` +**Rules:** Fundamentals and primitive reference types: zero → `false`, non-zero → `true`. +References: `null` → `false`, non-null → `true` ### Unsafe Casting @@ -126,17 +188,21 @@ unsafe { ## Passing Semantics -**Primitive types** are passed by value (copied): +**Fundamental types** (`int`, `float`, `byte`, `char`, `bool`, `pointer`) are passed by value (copied): ```ovum -fun ModifyInt(x: Int): Void { +fun ModifyInt(x: int): Void { x = x + 1 // Only modifies the local copy } ``` -**Reference types** are passed by reference: +**Primitive reference types** (`Int`, `Float`, `Byte`, `Char`, `Bool`, `Pointer`) and **all other reference types** (including `String`, arrays, and user-defined types) are passed by reference: ```ovum +fun ModifyIntRef(var x: Int): Void { + x = x + 1 // Implicit conversion: Int + int -> Int +} + fun ModifyArray(arr: IntArray): Void { - arr[0] = 999 // Modifies the original array + arr[0] := Int(999) // Use := for deep copy assignment } ``` @@ -154,21 +220,28 @@ fun CanReassign(var str: String): Void { ## Type System Characteristics **Static typing:** Every variable and expression has a type checked at compile time -**No implicit conversions:** Explicit casting required between different types +**Limited implicit conversions:** The compiler only performs implicit conversions between a primitive reference type and its matching fundamental (for example, `Int` ↔ `int`). Any conversion across different primitive families—such as `Int` to `Float` or `Float` to `int`—must use an explicit cast. **Type safety:** Prevents many common errors -**Nullable types:** Any type can be made nullable by appending `?` +**Nullable types:** Any reference type (including primitive reference types) can be made nullable by appending `?`. Fundamental types cannot be nullable. ```ovum -val x: Int = 42 +val x: int = 42 val y: String = "Hello" -// val z: Int = x + y // ERROR: Cannot add Int and String +// val z: int = x + y // ERROR: Cannot add int and String + +val intVal: int = 42 +val floatVal: float = 3.14 +val result: float = (intVal as float) + floatVal // OK: Explicit conversion -val intVal: Int = 42 -val floatVal: Float = 3.14 -val result: Float = (intVal as Float) + floatVal // OK: Explicit conversion +// Using primitive reference types (implicit conversion between wrappers and fundamentals) +val refInt: Int = 42 // Implicit conversion from literal to Int +val refFloat: Float = 3.14 // Implicit conversion from literal to Float +val sum: Int = refInt + (refFloat as Int) // Requires explicit narrowing +val fundamentalSum: int = sum + 10 // Implicit: Int -> int when assigning to a fundamental -val nullableInt: Int? = null -val nullableString: String? = "Hello" +// Converting nullable primitives to fundamentals +val nullableInt: Int? = 42 // Implicit conversion from literal +val fundamentalFromNullable: int = (nullableInt ?: 0) as int // Two-step conversion ``` ## Pure Function Constraints @@ -189,11 +262,23 @@ Type information is preserved at runtime for reference types: ```ovum fun ProcessObject(obj: Object): Void { if (obj is String) { - val str: String = (obj as String)!! - sys::Print("String length: " + str.Length().ToString()) + val str: String? = obj as String + if (str != null) { + val nonNullStr: String = str ?: "default" // Use Elvis operator + sys::Print("String length: " + nonNullStr.Length().ToString()) + } } else if (obj is IntArray) { - val arr: IntArray = (obj as IntArray)!! - sys::Print("Array size: " + arr.Length().ToString()) + val arr: IntArray? = obj as IntArray + if (arr != null) { + val nonNullArr: IntArray = arr ?: IntArray(0) // Use Elvis operator + sys::Print("Array size: " + nonNullArr.Length().ToString()) + } + } else if (obj is Int) { + val intRef: Int? = obj as Int + if (intRef != null) { + val nonNullInt: Int = intRef ?: 0 // Use Elvis operator + sys::Print("Int value: " + nonNullInt.ToString()) // Implicit conversion to string + } } } ``` diff --git a/lib/mylib/MyClass.cpp b/lib/mylib/MyClass.cpp index 0fa82bf..7af8b55 100644 --- a/lib/mylib/MyClass.cpp +++ b/lib/mylib/MyClass.cpp @@ -1,6 +1,7 @@ #include "MyClass.hpp" -MyClass::MyClass(std::ostream& out) : out_(out) {} +MyClass::MyClass(std::ostream& out) : out_(out) { +} void MyClass::Print(const std::string& str) { out_ << str; diff --git a/lib/mylib/MyClass.hpp b/lib/mylib/MyClass.hpp index d2e41c0..2847929 100644 --- a/lib/mylib/MyClass.hpp +++ b/lib/mylib/MyClass.hpp @@ -4,13 +4,13 @@ #include class MyClass { - public: +public: explicit MyClass(std::ostream& out); void Print(const std::string& str); - private: +private: std::ostream& out_; }; -#endif //MYCLASS_HPP_ +#endif // MYCLASS_HPP_ diff --git a/lib/ui/ui_functions.cpp b/lib/ui/ui_functions.cpp index d60d467..81859b6 100644 --- a/lib/ui/ui_functions.cpp +++ b/lib/ui/ui_functions.cpp @@ -1,5 +1,5 @@ -#include "ui_functions.hpp" #include "lib/mylib/MyClass.hpp" +#include "ui_functions.hpp" int32_t StartConsoleUI(const std::vector& args, std::ostream& out) { if (args.size() < 2) { diff --git a/lib/ui/ui_functions.hpp b/lib/ui/ui_functions.hpp index 8d06352..5409bec 100644 --- a/lib/ui/ui_functions.hpp +++ b/lib/ui/ui_functions.hpp @@ -1,10 +1,10 @@ #ifndef UI_FUNCTIONS_HPP_ #define UI_FUNCTIONS_HPP_ -#include -#include #include +#include +#include int32_t StartConsoleUI(const std::vector& args, std::ostream& out); -#endif //UI_FUNCTIONS_HPP_ +#endif // UI_FUNCTIONS_HPP_ diff --git a/tests/test_functions.hpp b/tests/test_functions.hpp index 50a78bd..8c154f8 100644 --- a/tests/test_functions.hpp +++ b/tests/test_functions.hpp @@ -1,9 +1,9 @@ #ifndef TESTFUNCTIONS_HPP_ #define TESTFUNCTIONS_HPP_ -#include #include +#include std::vector SplitString(const std::string& str); -#endif //TESTFUNCTIONS_HPP_ \ No newline at end of file +#endif // TESTFUNCTIONS_HPP_ diff --git a/tests/test_suites/ProjectIntegrationTestSuite.hpp b/tests/test_suites/ProjectIntegrationTestSuite.hpp index 1b1d0bf..0ecab1e 100644 --- a/tests/test_suites/ProjectIntegrationTestSuite.hpp +++ b/tests/test_suites/ProjectIntegrationTestSuite.hpp @@ -13,4 +13,4 @@ struct ProjectIntegrationTestSuite : public testing::Test { // special test stru void TearDown() override; // method that is called at the end of every test }; -#endif //TEMPORARYDIRECTORYTESTSUITE_HPP_ +#endif // TEMPORARYDIRECTORYTESTSUITE_HPP_ diff --git a/tests/unit_tests.cpp b/tests/unit_tests.cpp index 8fd17f8..fdee417 100644 --- a/tests/unit_tests.cpp +++ b/tests/unit_tests.cpp @@ -1,8 +1,8 @@ #include #include -#include "test_functions.hpp" // include your library here #include "lib/mylib/MyClass.hpp" +#include "test_functions.hpp" // include your library here TEST(MyLibUnitTestSuite, BasicTest1) { std::ostringstream out; @@ -12,4 +12,4 @@ TEST(MyLibUnitTestSuite, BasicTest1) { ASSERT_EQ(out_by_words.size(), 2); ASSERT_EQ(out_by_words[0], "Hello,"); ASSERT_EQ(out_by_words[1], "World!"); -} \ No newline at end of file +}