diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..d0a2a7d --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,272 @@ +# Testing in Lux + +This guide explains how to write and run tests for Lux programs. + +## Test File Structure + +Lux uses a simple test framework based on comparing program output against expected results. Tests are organized in the `tests/` directory. + +### Expected Output Files + +Each `.lux` file in `examples/` can have a corresponding `.expected` file in `tests/expected/` that contains the expected output: + +``` +examples/ + hello.lux # The program +tests/ + expected/ + hello.expected # Expected output +``` + +## Running Tests + +### Running All Tests + +```bash +# Run the full test suite +cargo test + +# Run with output +cargo test -- --nocapture +``` + +### Running a Single Program + +```bash +# Run a specific file +./result/bin/lux examples/hello.lux + +# Or with cargo +cargo run -- examples/hello.lux +``` + +## Writing Tests + +### Basic Program Test + +Create a program in `examples/` and its expected output in `tests/expected/`: + +```lux +// examples/my_test.lux +fn main(): Unit with {Console} = { + Console.print("Hello, Test!") + Console.print("1 + 2 = " + toString(1 + 2)) +} + +let output = run main() with {} +``` + +Expected output (`tests/expected/my_test.expected`): +``` +Hello, Test! +1 + 2 = 3 +``` + +### Testing Pure Functions + +For testing pure functions, create a test file that prints results: + +```lux +// examples/test_math.lux +fn add(a: Int, b: Int): Int = a + b +fn multiply(a: Int, b: Int): Int = a * b + +fn assertEqual(expected: Int, actual: Int, name: String): Unit with {Console} = + if expected == actual then Console.print(name + ": PASS") + else Console.print(name + ": FAIL - expected " + toString(expected) + ", got " + toString(actual)) + +fn main(): Unit with {Console} = { + assertEqual(5, add(2, 3), "add(2, 3)") + assertEqual(6, multiply(2, 3), "multiply(2, 3)") + assertEqual(0, add(-1, 1), "add(-1, 1)") +} + +let output = run main() with {} +``` + +### Testing with Effects + +Test effect handlers by providing mock implementations: + +```lux +// Example: Testing with a mock Console +effect Console { + fn print(message: String): Unit +} + +// Real implementation would output to terminal +// Test implementation captures output to a list +handler testConsole(output: List): Console { + fn print(message) = { + // In a real test, we'd capture this + resume(()) + } +} + +fn testableFunction(): Unit with {Console} = { + Console.print("Test output") +} + +fn runTest(): Unit with {Console} = { + // Run with test handler + run testableFunction() with { + Console = testConsole([]) + } + Console.print("Test completed") +} + +let output = run runTest() with {} +``` + +### Testing Result Types + +Use pattern matching to test functions that return `Result`: + +```lux +fn testResults(): Unit with {Console} = { + let success = Ok(42) + let failure = Err("Something went wrong") + + match success { + Ok(n) => Console.print("Success with " + toString(n)), + Err(e) => Console.print("Unexpected error: " + e) + } + + match failure { + Ok(n) => Console.print("Unexpected success"), + Err(e) => Console.print("Expected error: " + e) + } +} + +let output = run testResults() with {} +``` + +### Testing Option Types + +```lux +fn findEven(list: List): Option = + List.find(list, fn(x: Int): Bool => x % 2 == 0) + +fn testOptions(): Unit with {Console} = { + match findEven([1, 3, 5]) { + None => Console.print("No even number found: PASS"), + Some(_) => Console.print("FAIL - found unexpected even") + } + + match findEven([1, 2, 3]) { + None => Console.print("FAIL - should find 2"), + Some(n) => Console.print("Found even: " + toString(n) + (if n == 2 then " PASS" else " FAIL")) + } +} + +let output = run testOptions() with {} +``` + +## Test Patterns + +### Assertion Helpers + +Create reusable assertion functions: + +```lux +fn assertEq(expected: T, actual: T, desc: String): Unit with {Console} = + Console.print(desc + ": " + (if expected == actual then "PASS" else "FAIL")) + +fn assertTrue(condition: Bool, desc: String): Unit with {Console} = + Console.print(desc + ": " + (if condition then "PASS" else "FAIL")) + +fn assertFalse(condition: Bool, desc: String): Unit with {Console} = + Console.print(desc + ": " + (if condition then "FAIL" else "PASS")) +``` + +### Grouping Tests + +Organize related tests into groups: + +```lux +fn testGroup(name: String): Unit with {Console} = { + Console.print("") + Console.print("=== " + name + " ===") +} + +fn runAllTests(): Unit with {Console} = { + testGroup("List Operations") + testListHead() + testListTail() + testListMap() + + testGroup("String Operations") + testStringLength() + testStringSplit() + + Console.print("") + Console.print("All tests completed") +} +``` + +## Example Test Files + +Look at the example projects for comprehensive test patterns: + +- `projects/json-parser/main.lux` - Tests recursive parsing +- `projects/todo-app/main.lux` - Tests ADTs and list operations +- `projects/mini-interpreter/main.lux` - Tests complex pattern matching +- `projects/markdown-converter/main.lux` - Tests string manipulation + +## CI/CD Integration + +The test suite runs automatically on every commit. To run locally: + +```bash +# Build and test +nix build +nix develop --command cargo test + +# Test all example programs +for f in examples/standard/*.lux; do + echo "Testing $f" + ./result/bin/lux "$f" > /tmp/out.txt 2>&1 + if [ $? -eq 0 ]; then + echo " OK" + else + echo " FAILED" + cat /tmp/out.txt + fi +done +``` + +## Debugging Tests + +### Verbose Output + +Add debug prints to trace execution: + +```lux +fn debug(msg: String): Unit with {Console} = + Console.print("[DEBUG] " + msg) + +fn myFunction(x: Int): Int with {Console} = { + debug("Input: " + toString(x)) + let result = x * 2 + debug("Output: " + toString(result)) + result +} +``` + +### Type Checking Only + +Check types without running: + +```bash +# Type check only (when available) +./result/bin/lux --check examples/myfile.lux +``` + +## Best Practices + +1. **One concern per test** - Each test should verify one specific behavior +2. **Descriptive names** - Use clear test function names that describe what's being tested +3. **Expected output** - Create `.expected` files for reproducible testing +4. **Test edge cases** - Include tests for empty lists, zero values, error conditions +5. **Keep tests fast** - Avoid complex computations in tests +6. **Test effects separately** - Mock effects to isolate behavior being tested diff --git a/projects/json-parser/main.lux b/projects/json-parser/main.lux new file mode 100644 index 0000000..0eacc74 --- /dev/null +++ b/projects/json-parser/main.lux @@ -0,0 +1,226 @@ +// JSON Parser - Stress tests recursion, ADTs, and string manipulation +// +// This demonstrates: +// - Recursive descent parsing +// - Complex ADTs for AST representation +// - Character handling with Char type +// - Error handling with Result type + +// JSON Value representation +type JsonValue = + | JsonNull + | JsonBool(Bool) + | JsonNumber(Int) + | JsonString(String) + | JsonArray(List) + +// Parser state: input chars and position +type ParseState = + | ParseState(List, Int) + +// Parser result +type ParseResult = + | ParseOk(T, ParseState) + | ParseErr(String) + +// Get current character +fn peek(state: ParseState): Option = + match state { + ParseState(chars, pos) => List.get(chars, pos) + } + +// Advance position +fn advance(state: ParseState): ParseState = + match state { + ParseState(chars, pos) => ParseState(chars, pos + 1) + } + +// Skip whitespace +fn skipWhitespace(state: ParseState): ParseState = + match peek(state) { + None => state, + Some(c) => + if c == ' ' || c == '\t' || c == '\n' then + skipWhitespace(advance(state)) + else state + } + +// Check if character is a digit +fn isDigit(c: Char): Bool = + c == '0' || c == '1' || c == '2' || c == '3' || c == '4' || + c == '5' || c == '6' || c == '7' || c == '8' || c == '9' + +fn digitValue(c: Char): Int = + if c == '0' then 0 + else if c == '1' then 1 + else if c == '2' then 2 + else if c == '3' then 3 + else if c == '4' then 4 + else if c == '5' then 5 + else if c == '6' then 6 + else if c == '7' then 7 + else if c == '8' then 8 + else if c == '9' then 9 + else 0 + +// Parse a number +fn parseNumber(state: ParseState): ParseResult = + parseDigits(state, 0, false) + +fn parseDigits(state: ParseState, acc: Int, hasDigits: Bool): ParseResult = + match peek(state) { + None => + if hasDigits then ParseOk(acc, state) + else ParseErr("Expected digit"), + Some(c) => + if isDigit(c) then + parseDigits(advance(state), acc * 10 + digitValue(c), true) + else if hasDigits then ParseOk(acc, state) + else ParseErr("Expected digit") + } + +// Parse a string (simple version - no escapes) +fn parseString(state: ParseState): ParseResult = + match peek(state) { + None => ParseErr("Unexpected end"), + Some(c) => + if c != '"' then ParseErr("Expected quote") + else parseStringContent(advance(state), "") + } + +fn charToString(c: Char): String = + String.fromChar(c) + +fn parseStringContent(state: ParseState, acc: String): ParseResult = + match peek(state) { + None => ParseErr("Unterminated string"), + Some(c) => + if c == '"' then ParseOk(acc, advance(state)) + else parseStringContent(advance(state), acc + charToString(c)) + } + +// Check if next chars match a keyword +fn matchKeyword(state: ParseState, keyword: String): Bool = + matchKeywordChars(state, String.chars(keyword), 0) + +fn matchKeywordChars(state: ParseState, keywordChars: List, idx: Int): Bool = + match List.get(keywordChars, idx) { + None => true, + Some(kc) => + match state { + ParseState(chars, pos) => + match List.get(chars, pos + idx) { + None => false, + Some(ic) => if ic == kc then matchKeywordChars(state, keywordChars, idx + 1) else false + } + } + } + +fn advanceBy(state: ParseState, n: Int): ParseState = + if n <= 0 then state + else advanceBy(advance(state), n - 1) + +// Main value parser +fn parseValue(state: ParseState): ParseResult = { + let s = skipWhitespace(state) + match peek(s) { + None => ParseErr("Unexpected end of input"), + Some(c) => + if matchKeyword(s, "null") then + ParseOk(JsonNull, advanceBy(s, 4)) + else if matchKeyword(s, "true") then + ParseOk(JsonBool(true), advanceBy(s, 4)) + else if matchKeyword(s, "false") then + ParseOk(JsonBool(false), advanceBy(s, 5)) + else if c == '"' then + match parseString(s) { + ParseErr(e) => ParseErr(e), + ParseOk(str, newState) => ParseOk(JsonString(str), newState) + } + else if c == '[' then parseArray(s) + else if isDigit(c) then + match parseNumber(s) { + ParseErr(e) => ParseErr(e), + ParseOk(num, newState) => ParseOk(JsonNumber(num), newState) + } + else ParseErr("Unexpected character") + } +} + +// Parse array +fn parseArray(state: ParseState): ParseResult = + match peek(state) { + None => ParseErr("Unexpected end"), + Some(c) => + if c != '[' then ParseErr("Expected [") + else parseArrayElements(skipWhitespace(advance(state)), []) + } + +fn parseArrayElements(state: ParseState, acc: List): ParseResult = + match peek(state) { + None => ParseErr("Unterminated array"), + Some(c) => + if c == ']' then ParseOk(JsonArray(acc), advance(state)) + else match parseValue(state) { + ParseErr(e) => ParseErr(e), + ParseOk(value, newState) => { + let afterComma = skipWhitespace(newState) + match peek(afterComma) { + None => ParseErr("Unterminated array"), + Some(next) => + if next == ']' then ParseOk(JsonArray(List.concat(acc, [value])), advance(afterComma)) + else if next == ',' then parseArrayElements(skipWhitespace(advance(afterComma)), List.concat(acc, [value])) + else ParseErr("Expected , or ]") + } + } + } + } + +// Public parse function +fn parse(input: String): Result = + match parseValue(ParseState(String.chars(input), 0)) { + ParseErr(e) => Err(e), + ParseOk(value, _) => Ok(value) + } + +// Pretty print JSON +fn valueToString(value: JsonValue): String = + match value { + JsonNull => "null", + JsonBool(b) => if b then "true" else "false", + JsonNumber(n) => toString(n), + JsonString(s) => "\"" + s + "\"", + JsonArray(items) => "[" + String.join(List.map(items, valueToString), ", ") + "]" + } + +// Test the parser +fn main(): Unit with {Console} = { + Console.print("=== JSON Parser Test ===") + Console.print("") + + Console.print("Test 1: Simple values") + testParse("null") + testParse("true") + testParse("false") + testParse("42") + testParse("\"hello\"") + Console.print("") + + Console.print("Test 2: Arrays") + testParse("[]") + testParse("[1, 2, 3]") + testParse("[true, false, null]") + testParse("[1, [2, 3], 4]") + Console.print("") + + Console.print("Test 3: Nested arrays") + testParse("[[1, 2], [3, 4]]") +} + +fn testParse(input: String): Unit with {Console} = + match parse(input) { + Ok(value) => Console.print(" " + input + " => " + valueToString(value)), + Err(e) => Console.print(" ERROR: " + e) + } + +let output = run main() with {} diff --git a/projects/markdown-converter/main.lux b/projects/markdown-converter/main.lux new file mode 100644 index 0000000..0440dfe --- /dev/null +++ b/projects/markdown-converter/main.lux @@ -0,0 +1,79 @@ +// Markdown to HTML Converter - Stress tests string manipulation and parsing +// +// This demonstrates: +// - String parsing and manipulation +// - ADTs for document structure +// - Recursive processing +// - Pattern matching on strings + +// Markdown AST +type MarkdownNode = + | MHeading(Int, String) + | MParagraph(String) + | MList(Bool, List) + | MHorizontalRule + +// Parse a line to determine block type +fn parseBlockType(line: String): MarkdownNode = + if String.startsWith(line, "# ") then + MHeading(1, String.substring(line, 2, String.length(line))) + else if String.startsWith(line, "## ") then + MHeading(2, String.substring(line, 3, String.length(line))) + else if String.startsWith(line, "### ") then + MHeading(3, String.substring(line, 4, String.length(line))) + else if String.startsWith(line, "---") then + MHorizontalRule + else if String.startsWith(line, "- ") then + MList(false, [String.substring(line, 2, String.length(line))]) + else + MParagraph(line) + +// Convert to HTML +fn nodeToHtml(node: MarkdownNode): String = + match node { + MHeading(level, text) => "" + text + "", + MParagraph(text) => if text == "" then "" else "

" + text + "

", + MList(ordered, items) => { + let tag = if ordered then "ol" else "ul" + let itemsHtml = List.map(items, fn(item: String): String => "
  • " + item + "
  • ") + "<" + tag + ">" + String.join(itemsHtml, "") + "" + }, + MHorizontalRule => "
    " + } + +// Convert markdown string to HTML +fn convert(markdown: String): String = { + let lines = String.lines(markdown) + let nodes = List.map(lines, parseBlockType) + let htmlLines = List.filter(List.map(nodes, nodeToHtml), fn(s: String): Bool => s != "") + String.join(htmlLines, "\n") +} + +// Test the converter +fn main(): Unit with {Console} = { + Console.print("========================================") + Console.print(" MARKDOWN TO HTML CONVERTER") + Console.print("========================================") + Console.print("") + + Console.print("Test 1: Headings") + Console.print(convert("# Heading 1")) + Console.print(convert("## Heading 2")) + Console.print(convert("### Heading 3")) + Console.print("") + + Console.print("Test 2: Paragraphs") + Console.print(convert("This is a paragraph.")) + Console.print(convert("Another paragraph here.")) + Console.print("") + + Console.print("Test 3: Lists") + Console.print(convert("- Item 1")) + Console.print(convert("- Item 2")) + Console.print("") + + Console.print("Test 4: Horizontal rule") + Console.print(convert("---")) +} + +let output = run main() with {} diff --git a/projects/mini-interpreter/main.lux b/projects/mini-interpreter/main.lux new file mode 100644 index 0000000..39c5e01 --- /dev/null +++ b/projects/mini-interpreter/main.lux @@ -0,0 +1,180 @@ +// Mini Interpreter - Stress tests complex ADTs, recursion, and pattern matching +// +// This demonstrates: +// - Abstract syntax trees +// - Recursive evaluation +// - Pattern matching on complex types +// - Error handling + +// Value types in our mini language +type Value = + | VInt(Int) + | VBool(Bool) + | VString(String) + | VUnit + +// Expression AST +type Expr = + | EInt(Int) + | EBool(Bool) + | EString(String) + | EVar(String) + | EAdd(Expr, Expr) + | ESub(Expr, Expr) + | EMul(Expr, Expr) + | EDiv(Expr, Expr) + | EEq(Expr, Expr) + | ELt(Expr, Expr) + | EGt(Expr, Expr) + | EIf(Expr, Expr, Expr) + | ELet(String, Expr, Expr) + +// Environment (variable bindings) +type Env = + | EmptyEnv + | ExtendEnv(String, Value, Env) + +// Environment operations +fn envLookup(env: Env, name: String): Option = + match env { + EmptyEnv => None, + ExtendEnv(n, v, rest) => if n == name then Some(v) else envLookup(rest, name) + } + +fn envExtend(env: Env, name: String, value: Value): Env = + ExtendEnv(name, value, env) + +// Value pretty printing +fn valueToString(v: Value): String = + match v { + VInt(n) => toString(n), + VBool(b) => if b then "true" else "false", + VString(s) => "\"" + s + "\"", + VUnit => "()" + } + +// Main evaluator +fn eval(expr: Expr, env: Env): Result = + match expr { + EInt(n) => Ok(VInt(n)), + EBool(b) => Ok(VBool(b)), + EString(s) => Ok(VString(s)), + EVar(name) => match envLookup(env, name) { + None => Err("Unbound variable: " + name), + Some(v) => Ok(v) + }, + EAdd(left, right) => evalBinOp(left, right, env, fn(a: Int, b: Int): Int => a + b), + ESub(left, right) => evalBinOp(left, right, env, fn(a: Int, b: Int): Int => a - b), + EMul(left, right) => evalBinOp(left, right, env, fn(a: Int, b: Int): Int => a * b), + EDiv(left, right) => evalDiv(left, right, env), + EEq(left, right) => evalCompare(left, right, env, fn(a: Int, b: Int): Bool => a == b), + ELt(left, right) => evalCompare(left, right, env, fn(a: Int, b: Int): Bool => a < b), + EGt(left, right) => evalCompare(left, right, env, fn(a: Int, b: Int): Bool => a > b), + EIf(cond, thenBranch, elseBranch) => evalIf(cond, thenBranch, elseBranch, env), + ELet(name, valueExpr, bodyExpr) => evalLet(name, valueExpr, bodyExpr, env) + } + +fn evalBinOp(left: Expr, right: Expr, env: Env, op: fn(Int, Int): Int): Result = + match eval(left, env) { + Err(e) => Err(e), + Ok(leftVal) => match eval(right, env) { + Err(e) => Err(e), + Ok(rightVal) => match (leftVal, rightVal) { + (VInt(a), VInt(b)) => Ok(VInt(op(a, b))), + (_, _) => Err("Type error: expected Int") + } + } + } + +fn evalDiv(left: Expr, right: Expr, env: Env): Result = + match eval(left, env) { + Err(e) => Err(e), + Ok(leftVal) => match eval(right, env) { + Err(e) => Err(e), + Ok(rightVal) => match (leftVal, rightVal) { + (VInt(a), VInt(b)) => if b == 0 then Err("Division by zero") else Ok(VInt(a / b)), + (_, _) => Err("Type error: expected Int") + } + } + } + +fn evalCompare(left: Expr, right: Expr, env: Env, op: fn(Int, Int): Bool): Result = + match eval(left, env) { + Err(e) => Err(e), + Ok(leftVal) => match eval(right, env) { + Err(e) => Err(e), + Ok(rightVal) => match (leftVal, rightVal) { + (VInt(a), VInt(b)) => Ok(VBool(op(a, b))), + (_, _) => Err("Type error: expected Int") + } + } + } + +fn evalIf(cond: Expr, thenBranch: Expr, elseBranch: Expr, env: Env): Result = + match eval(cond, env) { + Err(e) => Err(e), + Ok(condVal) => match condVal { + VBool(true) => eval(thenBranch, env), + VBool(false) => eval(elseBranch, env), + _ => Err("Condition must be boolean") + } + } + +fn evalLet(name: String, valueExpr: Expr, bodyExpr: Expr, env: Env): Result = + match eval(valueExpr, env) { + Err(e) => Err(e), + Ok(value) => eval(bodyExpr, envExtend(env, name, value)) + } + +// Run a program and print result +fn runProgram(expr: Expr): Unit with {Console} = + match eval(expr, EmptyEnv) { + Ok(value) => Console.print("=> " + valueToString(value)), + Err(e) => Console.print("Error: " + e) + } + +// Test programs +fn program1(): Expr = ELet("x", EInt(10), ELet("y", EInt(20), EAdd(EVar("x"), EVar("y")))) + +fn program2(): Expr = ELet("a", EInt(5), EMul(EVar("a"), EAdd(EVar("a"), EInt(1)))) + +fn program3(): Expr = EIf(EGt(EInt(10), EInt(5)), EString("yes"), EString("no")) + +fn program4(): Expr = ELet("n", EInt(5), ELet("a", EMul(EVar("n"), ESub(EVar("n"), EInt(1))), ELet("b", EMul(EVar("a"), ESub(EVar("n"), EInt(2))), ELet("c", EMul(EVar("b"), ESub(EVar("n"), EInt(3))), EMul(EVar("c"), ESub(EVar("n"), EInt(4))))))) + +fn program5(): Expr = EDiv(EInt(10), EInt(0)) + +fn program6(): Expr = EVar("undefined") + +// Main +fn main(): Unit with {Console} = { + Console.print("========================================") + Console.print(" MINI INTERPRETER DEMO") + Console.print("========================================") + Console.print("") + + Console.print("Program 1: let x = 10 in let y = 20 in x + y") + runProgram(program1()) + Console.print("") + + Console.print("Program 2: let a = 5 in a * (a + 1)") + runProgram(program2()) + Console.print("") + + Console.print("Program 3: if 10 > 5 then \"yes\" else \"no\"") + runProgram(program3()) + Console.print("") + + Console.print("Program 4: 5! computed iteratively") + runProgram(program4()) + Console.print("") + + Console.print("Program 5: Division by zero error") + runProgram(program5()) + Console.print("") + + Console.print("Program 6: Unbound variable error") + runProgram(program6()) +} + +let output = run main() with {} diff --git a/projects/todo-app/main.lux b/projects/todo-app/main.lux new file mode 100644 index 0000000..82d8723 --- /dev/null +++ b/projects/todo-app/main.lux @@ -0,0 +1,123 @@ +// Todo App - Stress tests ADTs, pattern matching, and list operations +// +// This demonstrates: +// - ADTs for data modeling +// - Pattern matching +// - List operations +// - Recursive display + +// Todo item +type Priority = + | Low + | Medium + | High + +type TodoItem = + | TodoItem(Int, String, Bool, Priority) + +// Helper functions for TodoItem +fn getId(item: TodoItem): Int = + match item { TodoItem(id, _, _, _) => id } + +fn getDescription(item: TodoItem): String = + match item { TodoItem(_, desc, _, _) => desc } + +fn isCompleted(item: TodoItem): Bool = + match item { TodoItem(_, _, completed, _) => completed } + +fn getPriority(item: TodoItem): Priority = + match item { TodoItem(_, _, _, priority) => priority } + +fn setCompleted(item: TodoItem, completed: Bool): TodoItem = + match item { TodoItem(id, desc, _, priority) => TodoItem(id, desc, completed, priority) } + +// Priority helpers +fn priorityToString(p: Priority): String = + match p { + Low => "Low", + Medium => "Medium", + High => "High" + } + +fn priorityToSymbol(p: Priority): String = + match p { + Low => ".", + Medium => "*", + High => "!" + } + +// Format a todo item for display +fn formatItem(item: TodoItem): String = + toString(getId(item)) + ". " + (if isCompleted(item) then "[x]" else "[ ]") + " " + priorityToSymbol(getPriority(item)) + " " + getDescription(item) + +// Display functions using recursion +fn printItemsHelper(items: List): Unit with {Console} = + match List.head(items) { + None => (), + Some(item) => { + Console.print(" " + formatItem(item)) + match List.tail(items) { + None => (), + Some(rest) => printItemsHelper(rest) + } + } + } + +fn displayItems(items: List): Unit with {Console} = + if List.isEmpty(items) then Console.print(" (no items)") + else printItemsHelper(items) + +fn countCompleted(items: List): Int = + List.length(List.filter(items, isCompleted)) + +fn displayStats(items: List): Unit with {Console} = + Console.print("Total: " + toString(List.length(items)) + " | Completed: " + toString(countCompleted(items)) + " | Pending: " + toString(List.length(items) - countCompleted(items))) + +// Sample data - all on one line to avoid multiline list parse issues +fn createSampleData(): List = [TodoItem(1, "Learn Lux basics", true, High), TodoItem(2, "Build a project", false, High), TodoItem(3, "Read documentation", true, Medium), TodoItem(4, "Write tests", false, Medium), TodoItem(5, "Share with team", false, Low)] + +// Filter functions +fn filterCompleted(items: List): List = + List.filter(items, isCompleted) + +fn filterPending(items: List): List = + List.filter(items, fn(item: TodoItem): Bool => isCompleted(item) == false) + +// Main demo +fn main(): Unit with {Console} = { + Console.print("========================================") + Console.print(" TODO APP DEMO") + Console.print("========================================") + Console.print("") + + let items = createSampleData() + + Console.print("All Todo Items:") + Console.print("---------------") + displayItems(items) + Console.print("") + displayStats(items) + Console.print("") + + Console.print("Completed Items:") + Console.print("----------------") + displayItems(filterCompleted(items)) + Console.print("") + + Console.print("Pending Items:") + Console.print("--------------") + displayItems(filterPending(items)) + Console.print("") + + Console.print("Marking item 2 as complete...") + let updatedItems = List.map(items, fn(item: TodoItem): TodoItem => if getId(item) == 2 then setCompleted(item, true) else item) + Console.print("") + + Console.print("Updated Todo List:") + Console.print("------------------") + displayItems(updatedItems) + Console.print("") + displayStats(updatedItems) +} + +let output = run main() with {} diff --git a/src/interpreter.rs b/src/interpreter.rs index f3eaaa8..7de51b2 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -83,6 +83,7 @@ pub enum BuiltinFn { StringToUpper, StringToLower, StringSubstring, + StringFromChar, // JSON operations JsonParse, @@ -809,6 +810,10 @@ impl Interpreter { "substring".to_string(), Value::Builtin(BuiltinFn::StringSubstring), ), + ( + "fromChar".to_string(), + Value::Builtin(BuiltinFn::StringFromChar), + ), ])); env.define("String", string_module); @@ -2293,6 +2298,17 @@ impl Interpreter { Ok(EvalResult::Value(Value::String(result))) } + BuiltinFn::StringFromChar => { + if args.len() != 1 { + return Err(err("String.fromChar requires 1 argument: char")); + } + let c = match &args[0] { + Value::Char(c) => *c, + v => return Err(err(&format!("String.fromChar expects Char, got {}", v.type_name()))), + }; + Ok(EvalResult::Value(Value::String(c.to_string()))) + } + // JSON operations BuiltinFn::JsonParse => { let s = Self::expect_arg_1::(&args, "Json.parse", span)?; diff --git a/src/types.rs b/src/types.rs index 2bc9f1c..be4d90a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1307,6 +1307,10 @@ impl TypeEnv { "substring".to_string(), Type::function(vec![Type::String, Type::Int, Type::Int], Type::String), ), + ( + "fromChar".to_string(), + Type::function(vec![Type::Char], Type::String), + ), ]); env.bind("String", TypeScheme::mono(string_module_type));