feat: add stress test projects and testing documentation
- Add String.fromChar function to convert Char to String - Create four stress test projects demonstrating Lux features: - json-parser: recursive descent parsing with Char handling - markdown-converter: string manipulation and ADTs - todo-app: list operations and pattern matching - mini-interpreter: AST evaluation and environments - Add comprehensive testing documentation (docs/testing.md) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
272
docs/testing.md
Normal file
272
docs/testing.md
Normal file
@@ -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<String>): 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<Int>): Option<Int> =
|
||||||
|
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<T>(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
|
||||||
226
projects/json-parser/main.lux
Normal file
226
projects/json-parser/main.lux
Normal file
@@ -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<JsonValue>)
|
||||||
|
|
||||||
|
// Parser state: input chars and position
|
||||||
|
type ParseState =
|
||||||
|
| ParseState(List<Char>, Int)
|
||||||
|
|
||||||
|
// Parser result
|
||||||
|
type ParseResult<T> =
|
||||||
|
| ParseOk(T, ParseState)
|
||||||
|
| ParseErr(String)
|
||||||
|
|
||||||
|
// Get current character
|
||||||
|
fn peek(state: ParseState): Option<Char> =
|
||||||
|
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<Int> =
|
||||||
|
parseDigits(state, 0, false)
|
||||||
|
|
||||||
|
fn parseDigits(state: ParseState, acc: Int, hasDigits: Bool): ParseResult<Int> =
|
||||||
|
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<String> =
|
||||||
|
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<String> =
|
||||||
|
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<Char>, 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<JsonValue> = {
|
||||||
|
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<JsonValue> =
|
||||||
|
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<JsonValue>): ParseResult<JsonValue> =
|
||||||
|
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<JsonValue, String> =
|
||||||
|
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 {}
|
||||||
79
projects/markdown-converter/main.lux
Normal file
79
projects/markdown-converter/main.lux
Normal file
@@ -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<String>)
|
||||||
|
| 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) => "<h" + toString(level) + ">" + text + "</h" + toString(level) + ">",
|
||||||
|
MParagraph(text) => if text == "" then "" else "<p>" + text + "</p>",
|
||||||
|
MList(ordered, items) => {
|
||||||
|
let tag = if ordered then "ol" else "ul"
|
||||||
|
let itemsHtml = List.map(items, fn(item: String): String => "<li>" + item + "</li>")
|
||||||
|
"<" + tag + ">" + String.join(itemsHtml, "") + "</" + tag + ">"
|
||||||
|
},
|
||||||
|
MHorizontalRule => "<hr />"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {}
|
||||||
180
projects/mini-interpreter/main.lux
Normal file
180
projects/mini-interpreter/main.lux
Normal file
@@ -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<Value> =
|
||||||
|
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<Value, String> =
|
||||||
|
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<Value, String> =
|
||||||
|
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<Value, String> =
|
||||||
|
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<Value, String> =
|
||||||
|
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<Value, String> =
|
||||||
|
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<Value, String> =
|
||||||
|
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 {}
|
||||||
123
projects/todo-app/main.lux
Normal file
123
projects/todo-app/main.lux
Normal file
@@ -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<TodoItem>): 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<TodoItem>): Unit with {Console} =
|
||||||
|
if List.isEmpty(items) then Console.print(" (no items)")
|
||||||
|
else printItemsHelper(items)
|
||||||
|
|
||||||
|
fn countCompleted(items: List<TodoItem>): Int =
|
||||||
|
List.length(List.filter(items, isCompleted))
|
||||||
|
|
||||||
|
fn displayStats(items: List<TodoItem>): 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> = [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<TodoItem>): List<TodoItem> =
|
||||||
|
List.filter(items, isCompleted)
|
||||||
|
|
||||||
|
fn filterPending(items: List<TodoItem>): List<TodoItem> =
|
||||||
|
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 {}
|
||||||
@@ -83,6 +83,7 @@ pub enum BuiltinFn {
|
|||||||
StringToUpper,
|
StringToUpper,
|
||||||
StringToLower,
|
StringToLower,
|
||||||
StringSubstring,
|
StringSubstring,
|
||||||
|
StringFromChar,
|
||||||
|
|
||||||
// JSON operations
|
// JSON operations
|
||||||
JsonParse,
|
JsonParse,
|
||||||
@@ -809,6 +810,10 @@ impl Interpreter {
|
|||||||
"substring".to_string(),
|
"substring".to_string(),
|
||||||
Value::Builtin(BuiltinFn::StringSubstring),
|
Value::Builtin(BuiltinFn::StringSubstring),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"fromChar".to_string(),
|
||||||
|
Value::Builtin(BuiltinFn::StringFromChar),
|
||||||
|
),
|
||||||
]));
|
]));
|
||||||
env.define("String", string_module);
|
env.define("String", string_module);
|
||||||
|
|
||||||
@@ -2293,6 +2298,17 @@ impl Interpreter {
|
|||||||
Ok(EvalResult::Value(Value::String(result)))
|
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
|
// JSON operations
|
||||||
BuiltinFn::JsonParse => {
|
BuiltinFn::JsonParse => {
|
||||||
let s = Self::expect_arg_1::<String>(&args, "Json.parse", span)?;
|
let s = Self::expect_arg_1::<String>(&args, "Json.parse", span)?;
|
||||||
|
|||||||
@@ -1307,6 +1307,10 @@ impl TypeEnv {
|
|||||||
"substring".to_string(),
|
"substring".to_string(),
|
||||||
Type::function(vec![Type::String, Type::Int, Type::Int], Type::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));
|
env.bind("String", TypeScheme::mono(string_module_type));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user