pax_global_header00006660000000000000000000000064151223544240014514gustar00rootroot0000000000000052 comment=be756212b9c8c04d98e5ce041263775fb04f8697 tomo-koans/000077500000000000000000000000001512235442400131475ustar00rootroot00000000000000tomo-koans/.gitignore000066400000000000000000000000331512235442400151330ustar00rootroot00000000000000editor.txt /lessons .build tomo-koans/README.md000066400000000000000000000002251512235442400144250ustar00rootroot00000000000000# Tomo Koans This is a program to help you learn the Tomo programming language by walking you through some example programs. ``` tomo koans.tm ``` tomo-koans/koans.tm000066400000000000000000000207311512235442400146270ustar00rootroot00000000000000# This is the Tomo koans program _HELP := "tomo koans - A tutorial program for learning the Tomo programming language" use colorful use shell use commands editor := "vim" user : Text? LESSONS := [ Lesson((./lessons/lesson-01-hello-world.tm), "Hello World", "Hello world\n"), Lesson((./lessons/lesson-02-tests.tm), "Testing Code"), Lesson((./lessons/lesson-03-variables.tm), "Variables"), Lesson((./lessons/lesson-04-functions.tm), "Functions"), Lesson((./lessons/lesson-05-basic-types.tm), "Basic Types"), Lesson((./lessons/lesson-06-arrays.tm), "Arrays"), Lesson((./lessons/lesson-07-optionals.tm), "Optionals"), Lesson((./lessons/lesson-08-tables.tm), "Tables"), Lesson((./lessons/lesson-09-text.tm), "Text"), Lesson((./lessons/lesson-10-structs.tm), "Structs"), Lesson((./lessons/lesson-11-enums.tm), "Enums"), Lesson((./lessons/lesson-12-allocating.tm), "Allocating Memory"), Lesson((./lessons/lesson-13-paths.tm), "File Paths"), Lesson((./lessons/lesson-14-langs.tm), "Embedded Languages"), Lesson((./lessons/lesson-15-min-max.tm), "Min and Max"), Lesson((./lessons/lesson-16-reducers.tm), "Reducers"), ] enum TestResult(NotRun, Success(output:Text), Error(err:Text), WrongOutput(actual:Text, expected:Text)) func print(result:TestResult) when result is NotRun pass is Success(s) $Colorful" @(b,u:Program Output:) @(green:$s) ".print() is Error(e) $Colorful" @(b,u:Program Errors:) $e ".print() is WrongOutput(actual, expected) $Colorful" @(b,u:Program Output:) @(red:$actual) @(b,u:Expected:) @(green:$expected) ".print() func is_success(result:TestResult -> Bool) when result is Success return yes else return no struct Lesson(file:Path, description:Text, expected_output:Text?=none) func get_result(l:Lesson -> TestResult) result := $Shell"COLOR=1 tomo -O 0 $(l.file)".result() if not result.succeeded() return Error(Text.from_utf8(result.errors)!) output := Text.from_utf8(result.output)! if expected := l.expected_output if output != expected return WrongOutput(output, expected) return Success(output) func ask_continue() _ := ask("\[2]Press Enter to continue...\[]", bold=no) func clear_screen() say("\x1b[2J\x1b[H", newline=no) func summarize_tests(results:[TestResult], highlight:Path?=none) $Colorful" @(yellow,b,u:Lessons) ".print() passing := 0 failing := 0 for i,lesson in LESSONS when results[i]! is Success passing += 1 $Colorful" @(green,bold:$(Text(i).left_pad(2)): "$(lesson.description)" (passes)) ".print() is NotRun failing += 1 $Colorful" @(dim:$(Text(i).left_pad(2)): "$(lesson.description)" (not yet attempted)) ".print() else failing += 1 $Colorful" @(red:$(Text(i).left_pad(2)): "$(lesson.description)" (failing)) ".print() completed := Num(passing)/Num(passing+failing) $Colorful" @(cyan,b:Progress: $(completed.percent())) ".print() func short_summarize_tests(results:[TestResult]) say("Progress: ", newline=no) for result in results when result is Success $Colorful"@(green,bold:#)".print(newline=no) is NotRun $Colorful"@(dim:#)".print(newline=no) else $Colorful"@(red:#)".print(newline=no) say("\n") func choose_option(options:{Text:Text} -> Text) repeat for k,v in options $Colorful" @(b:($k)) $v ".print() say("") choice := (ask("Choose an option: ") or goodbye()).lower().to(1) if options.has(choice) return choice else if choice == "q" goodbye() else $Colorful" @(red:I'm sorry, I don't recognize that choice, please try again!") ".print() fail("Unreachable") func show_lesson(lesson:Lesson, result:TestResult) clear_screen() $Colorful" @(yellow,b,u:$(lesson.description)) Here's what we have right now: ".print() result.print() when result is Success $Colorful" @(green,b:✨ Great job, this test is passing! ✨) ".print() is NotRun $Colorful" @(dim,italic:...nothing, edit the file to make your first attempt...) ".print() else $Colorful" @(red,b:Looks like this test isn't passing yet! 😢) ".print() func goodbye(-> Abort) clear_screen() $Colorful" @(b:Goodbye! Come back again soon!) ".print() exit(code=0) func give_feedback(section:Text) clear_screen() user = user or ask("What name do go by? (optional) ") or "anon" if user == "" then user = "anon" feedback := ask("What's your feedback? ") or return (./feedback.txt).append(" [$(user!)/$section] $feedback\n ")! func main(clean=no -> Abort) clear_screen() $Colorful" @(bold,green:Hello and welcome to the Tomo Koans program!) We're going to run through some programs that don't work and you'll be fixing them up! I hope you have fun! ".print() if clean (./editor.txt).remove(ignore_missing=yes)! (./lessons).remove(ignore_missing=yes)! if (./editor.txt).exists() editor = (./editor.txt).read()! $Colorful" @(dim,i:You're using @(green:$(editor)) as your text editor. If you want to change it, just edit @(magenta:./editor.txt)) ".print() else editor = ask(" What command line text editor do you want to use? Ones with syntax highlighting: vim, nvim, emacs Simpler editors: nano, pico, edit editor:$(" ") ") or goodbye() while editor == "" or not $Shell"command -v $editor >/dev/null".run().succeeded() editor = ask("I don't recognize that editor. Try again? ") or goodbye() (./editor.txt).write(editor)! $Colorful" Great! From now on, I'll use @(b:$(editor)) to edit files. If you want to change it, just edit @(magenta:./editor.txt) ".print() $Shell" cp -r lesson-templates lessons ".run().or_fail("Could not make lessons directory") test_results := &[TestResult.NotRun for l in LESSONS] ask_continue() message_pending := no repeat unless message_pending clear_screen() message_pending = no summarize_tests(test_results) choice := ask("Choose a test, (q)uit, or give (f)eedback: ") or stop repeat stop repeat if choice == "q" or choice == "Q" if choice == "f" or choice == "F" give_feedback("menu") skip if choice == "" for i,result in test_results if not result.is_success() choice = Text(i) stop n := Int.parse(choice) or (do $Colorful"@(red:I don't know what that means! Type a test number or 'q'.)".print() message_pending = yes skip repeat ) if n < 1 or n > LESSONS.length $Colorful"@(red:That's not a valid test number!)".print() message_pending = yes skip repeat repeat lesson := LESSONS[n]! show_lesson(lesson, test_results[n]!) short_summarize_tests(test_results) options := &{ "e": "Edit file and try again", "l": "Show the lesson list", "q": "Quit", "f": "Give Feedback", } if n < LESSONS.length options["n"] = "Go to the next lesson" when choose_option(options) is "e" $Shell" $(editor) $(lesson.file) ".run().or_fail("Could not open editor $(editor)") test_results[n] = lesson.get_result() test_results[n]!.print() is "l" stop is "n" n += 1 is "q" goodbye() is "f" give_feedback("lesson$n") goodbye() tomo-koans/lesson-templates/000077500000000000000000000000001512235442400164465ustar00rootroot00000000000000tomo-koans/lesson-templates/lesson-01-hello-world.tm000066400000000000000000000004441512235442400227610ustar00rootroot00000000000000# This is a simple "Hello world" program. # Lines beginning with a '#' are comments # Programs need a main() function to run: func main() # The language is indentation-sensitive, so indentation matters! # Edit this code so it prints the text "Hello world" say("Goodbye moon") tomo-koans/lesson-templates/lesson-02-tests.tm000066400000000000000000000006501512235442400216730ustar00rootroot00000000000000# Assertions func main() # Assertions can be used to check that a # value is what you expect it to be: assert 1 + 1 == 2 # For debugging purposes, you can use `>>` # to print out a value: >> 20 + 30 # If a docstring test's output is different # from what was expected, it will print an # error message and halt the program. # Edit this test so it passes: assert 2 + 2 == ??? tomo-koans/lesson-templates/lesson-03-variables.tm000066400000000000000000000010351512235442400225000ustar00rootroot00000000000000# Variables func main() # Variable declarations use `:=` and do not # require you to specify a type explicitly: x := 123 # To assign a new value, use `=` x = 999 # You can also use update assignments like `+=` x += 1 assert x == 1000 # Variables are strongly typed, so you can't # assign different types to the same variable: x = "hello" # <-- This will error, comment it out or fix it # Declare a variable called `y` and give it the # value "okay" ??? assert y == "okay" tomo-koans/lesson-templates/lesson-04-functions.tm000066400000000000000000000012251512235442400225420ustar00rootroot00000000000000# Functions func main() # Functions can be declared in any order. # The main() function is the first function to run, # but it can call any other functions defined in # the same file. # Here, we're calling a function defined below. # Fix up the function so it passes thes tests: assert add(5, 10) == 15 assert add(2, 4) == 6 # Functions can also be called with keyword arguments: assert add(x=4, y=12) == 16 # Functions are defined using `func` and must specify # the types of their arguments and return values like this: func add(x:Int, y:Int -> Int) # Fix this so it returns a sensible result: return ??? tomo-koans/lesson-templates/lesson-05-basic-types.tm000066400000000000000000000011141512235442400227530ustar00rootroot00000000000000# Basic Types func main() # Tomo has several built-in types, including: # - Int (integer numbers) # - Num (floating-point numbers) # - Text (string values) # - Bool (yes or no) # Fix these variables so they match the expected values: a := 42 b := 3.14 c := "Tomo" # Boolean values use `yes`/`no`, not `true`/`false` d := yes assert a == ??? assert b == ??? assert c == ??? assert d == ??? # Text values support interpolation using `$`: name := "Alice" greeting := "Hello $name" assert greeting == ??? tomo-koans/lesson-templates/lesson-06-arrays.tm000066400000000000000000000012711512235442400220360ustar00rootroot00000000000000# Arrays func main() # Arrays are ordered collections of values. # You can define an array using `[...]`: nums := [10, 20, 30] # Arrays are 1-indexed. assert nums[2] == ??? # Arrays can be empty but must have a type: empty : [Int] = [] assert empty == [] # You can loop over an array with `for value in array`: sum := 0 for num in nums sum += num assert sum == ??? # Array comprehensions let you transform arrays concisely: squares := [n*n for n in nums] assert squares == [???, ???, ???] # You can also get the index with `for index, value in array`: for i, num in nums assert squares[i] == num * num tomo-koans/lesson-templates/lesson-07-optionals.tm000066400000000000000000000031751512235442400225530ustar00rootroot00000000000000# Optional Types func main() # Any type can be optional, which means it will either be # `none` or have a value. You can declare an optional variable # and initialize it to `none` like this: y : Int? = none assert y == ??? # Optional variables can be either `none` or a value of that type: y = 123 assert y == 123 # Some functions return optional values. # Int.parse(text:Text -> Int?) is a function that returns # an integer value found in the text, or `none` if no integer # is found. assert Int.parse("123") == ??? assert Int.parse("blah") == ??? # You can check if a value exists with `if`: n := Int.parse("123") if n # Inside this condition, `n` is known to be non-none n = add(n, 1) assert n == ??? # Optionals are useful for handling missing data: name : Text? = none greeting := if name "Hello $name" else "Hello stranger" assert greeting == ??? # Optional values can be converted to non-optional using `or` assert Int.parse("blah") or 0 == ??? # They can also be converted using the `!` operator, which # will give an error if a non-none value is encountered: assert add(Int.parse("123")!, 1) == ??? # If you index into a list, the value is optional because # you might have used an index that isn't in the list: list := [10, 20, 30] assert list[1] == 10 assert list[999] == none # So, it's common to see `list[i]!` to force the value to # be non-optional or error if you made a mistake: assert list[3]! == ??? func add(x:Int, y:Int -> Int) return x + y tomo-koans/lesson-templates/lesson-08-tables.tm000066400000000000000000000014611512235442400220120ustar00rootroot00000000000000# Tables func main() # Tables store key-value pairs. # You can define a table using `{key: value, ...}`. scores := {"Alice": 100, "Bob": 200} assert scores == {"Alice": 100, "Bob": 200} assert scores["Alice"] == ??? # Accessing a missing key gives `none` assert scores["Zoltan"] == ??? # Tables can be empty but must have key and value types: empty : {Text:Int} = {} assert empty == {} # You can loop over tables: total := 0 for name, score in scores total += score assert total == ??? # Table keys and values can be accessed as an array: assert scores.keys == [???] assert scores.values == [???] # Table comprehensions let you create tables concisely: doubled := {k: v * 2 for k, v in scores} assert doubled == {???} tomo-koans/lesson-templates/lesson-09-text.tm000066400000000000000000000010671512235442400215270ustar00rootroot00000000000000# Text func main() # Text values are sequences of letters. greeting := "Hello" assert greeting.length == ??? # Text supports interpolation with `$`: name := "Alice" message := "Hello $name, your number is $(1 + 2)" assert message == ??? # Multi-line text uses indented quotes: multiline := " line one line two line three " # Method calls use `.` assert multiline.lines() == [???] # Common text methods: assert "hello".upper() == ??? assert "hello world".split(" ") == [???] tomo-koans/lesson-templates/lesson-10-structs.tm000066400000000000000000000015671512235442400222470ustar00rootroot00000000000000# Structs and Methods # The keyword `struct` is used to define structures # that hold multiple members: struct Point(x:Int, y:Int) # Methods are any function defined inside of the # indented area below a struct definition. # There is no implicit `self` argument, only the # arguments you explicitly define. func abs(p:Point -> Point) return Point(p.x.abs(), p.y.abs()) # Constants can be declared inside of a struct's namespace: ZERO := Point(0, 0) # Arbitrary functions can also be defined here: func squared_int(x:Int -> Int) return x * x func main() # You can create a struct instance like this: p := Point(x=3, y=-4) assert p == Point(x=???, y=???) assert Point.ZERO == Point(x=???, y=???) assert p.x == ??? assert p.y == ??? assert p.abs() == ??? assert Point.squared_int(5) == ??? tomo-koans/lesson-templates/lesson-11-enums.tm000066400000000000000000000015641512235442400216650ustar00rootroot00000000000000# Enums # Enums define a type with multiple possible variants: enum Shape(Circle(radius: Num), Rectangle(width: Num, height: Num), Point) # Use `when` to pattern match an enum: func area(shape: Shape -> Num) when shape is Circle(radius) return Num.PI * radius^2 is Rectangle(width, height) return width * height is Point return 0 func main() # You can create instances of an enum: point := Shape.Point # Single member enums display without the field names: circle := Shape.Circle(radius=10) assert circle == Shape.Circle(10) # Multi-member enums explicitly list their field names: rect := Shape.Rectangle(width=4, height=5) assert rect == Shape.Rectangle(width=4, height=5) assert point.area() == ??? assert rect.area() == ??? assert "My shape is $circle" == ??? tomo-koans/lesson-templates/lesson-12-allocating.tm000066400000000000000000000017261512235442400226540ustar00rootroot00000000000000# Heap Allocation with `@` func main() # By default, values in Tomo are immutable. # To allow mutation, you need to allocate memory using `@` nums := @[10, 20, 30] nums[1] = 99 assert nums[1] == ??? nums.insert(40) assert nums[4] == ??? # Allocated memory is not equal to other allocated memory: a := @[10, 20, 30] b := @[10, 20, 30] pointers_are_equal := (a == b) assert pointers_are_equal == ??? # The `[]` operator can be used to access the value stored # at a memory location: contents_are_equal := (a[] == b[]) assert contents_are_equal == ??? # Tables also require `@` to allow modifications: scores := @{"Alice": 100, "Bob": 200} scores["Charlie"] = 300 assert scores["Charlie"] == ??? # Without `@`, attempting to mutate will cause an error: frozen := {"key": "value"} frozen["key"] = "new value" # <- This will break, comment it out assert frozen["key"] == "value" tomo-koans/lesson-templates/lesson-13-paths.tm000066400000000000000000000011101512235442400216420ustar00rootroot00000000000000# Paths func main() # Tomo includes a built-in literal type for file paths # A path is inside parentheses and begins with `/`, `~`, `.` or `..` path := (/tmp/test-file.txt) assert path == (/tmp/test-file.txt) path.write("first")! assert path.read() == "???" path.append(",second")! assert path.exists() == yes assert path.parent()! == (/???) assert path.extension() == "???" assert path.parent()!.child("other-file.txt") == (/???) assert (/tmp/test-*.txt).glob() == [???] path.remove()! assert path.exists() == ??? tomo-koans/lesson-templates/lesson-14-langs.tm000066400000000000000000000012621512235442400216400ustar00rootroot00000000000000# Langs (Safe Embedded Languages) # `lang` defines custom text types with automatic escaping. lang HTML # Custom escaping rules can be created with `convert` convert(t:Text -> HTML) t = t.translate({"&": "&", "<": "<", ">": ">"}) return HTML.from_text(t) func paragraph(content:HTML -> HTML) return $HTML"

$content

" # Type safety prevents injection: func greet(name:HTML -> HTML) return $HTML"Hello $name!" func main() malicious_input := "hello" safe := $HTML"User said: $malicious_input" assert safe == ??? assert safe.paragraph() == ??? greeting := greet(malicious_input) # This won't compile tomo-koans/lesson-templates/lesson-15-min-max.tm000066400000000000000000000012061512235442400221010ustar00rootroot00000000000000# Min and Max struct Point(x:Int, y:Int) func main() # `_min_` and `_max_` return the smaller or larger of two values: smallest_int := 7 _min_ 3 assert smallest_int == ??? most_fruit := "apple" _max_ "banana" assert most_fruit == ??? # You can use an indexed key for choosing a maximum: biggest_y_point := Point(1, 2) _max_.y Point(999, 0) assert biggest_y_point == Point(???, ???) shortest_list := [1, 2, 3] _min_.length [99, 100] assert shortest_list == [???] # You can also use a method for choosing a maximum: biggest_magnitude := 5 _max_.abs() -999 assert biggest_magnitude == ??? tomo-koans/lesson-templates/lesson-16-reducers.tm000066400000000000000000000011671512235442400223560ustar00rootroot00000000000000# Reductions func main() # Reductions fold collections into a single value: assert (+: [1, 2, 3])! == ??? assert (*: [2, 3, 4])! == ??? # If an empty argument is given, a `none` value is returned empty : [Int] = [] assert (+: empty) == none # Use `or` to provide a fallback: assert (+: empty) or 100 == ??? # `_min_` and `_max_` work as reducers: assert (_max_: [10, 30, 20])! == ??? assert (_min_.length: ["x", "abcd", "yz"])! == "???" # Comprehensions inside reductions: assert (+: i * 2 for i in [1, 2, 3])! == ??? assert (+: i for i in 10 if i.is_prime())! == ??? tomo-koans/modules.ini000066400000000000000000000003331512235442400153170ustar00rootroot00000000000000[colorful] version=v2025-11-29 git=https://github.com/bruce-hill/tomo-colorful [shell] version=v2.1 git=https://github.com/bruce-hill/tomo-shell [commands] version=v2.1 git=https://github.com/bruce-hill/tomo-commands