// Tomo is a statically typed, garbage collected imperative language with // emphasis on simplicity, safety, and speed. Tomo code cross compiles to C, // which is compiled to a binary using your C compiler of choice. // To begin with, let's define a main function: func main(): // This function's code will run if you run this file. // Print to the console say("Hello world!") // Declare an integer variable (types are inferred) my_variable := 123 // Assign a new value my_variable = 99 // Floating point numbers are similar, but have a decimal point: my_num := 2.0 // Strings can use interpolation with curly braces: say("My variable is {my_variable}") // Docstring tests use ">>" and when the program runs, they will print // their source code to the console on stderr. >> 1 + 2 // If there is a following line with "=", you can perform a check that // the output matches what was expected. >> 2 + 3 = 5 // If there is a mismatch, the program will halt and print a useful // error message. // Booleans use "yes" and "no" instead of "true" and "false" my_bool := yes // Conditionals: if my_bool: say("It worked!") else if my_variable == 99: say("else if") else: say("else") // Arrays: my_numbers := [10, 20, 30] // Arrays are 1-indexed, so the first element is at index 1: >> my_numbers[1] = 10 // Negative indices can be used to get items from the back of the array: >> my_numbers[-1] = 30 // If an invalid index outside the array's bounds is used (e.g. // my_numbers[999]), an error message will be printed and the program will // exit. // Iteration: for num in my_numbers: >> num // Optionally, you can use an iteration index as well: for index, num in my_numbers: pass // Pass means "do nothing" // Loop control flow uses "skip" and "stop" for num in my_numbers: if num == 20: // You can specify which loop variable you're skipping/stopping if // there is any ambiguity. skip num if num == 30: stop >> num // Tables are efficient hash maps table := {"one": 1, "two": 2} >> table["two"] = 2 // Tables can be iterated over either by key or key,value: for key in table: pass for key, value in table: pass // Tables also have ".keys" and ".values" fields to explicitly access the // array of keys or values in the table. >> table.keys = ["one", "two"] >> table.values = [1, 2] // Tables can have default values and fallbacks: table2 := {"three": 3; fallback=table; default=0} >> table2["two"] = 2 >> table2["three"] = 3 >> table2["???"] = 0 // If no default is provided and a missing key is looked up, the program // will print an error message and halt. // Any types can be used in tables, for example, a table mapping arrays to // strings: >> {[10, 20]: "one", [30, 40, 50]: "two"} // So far, the datastructures that have been discussed are all *immutable*, // meaning you can't add, remove, or change their contents. If you want to // have mutable data, you need to allocate an area of memory which can hold // different values using the "@" operator (think: "@llocate"). my_arr := @[10, 20, 30] my_arr[1] = 999 >> my_arr = @[999, 20, 30] // To call a method, you must use ":" and the name of the method: my_arr:sort() >> my_arr = @[20, 30, 999] // To access the immutable value that resides inside the memory area, you // can use the "[]" operator: >> my_arr[] = [20, 30, 999] // You can think of this like taking a photograph of what's at that memory // location. Later, a new value might end up there, but the photograph will // remain unchanged. snapshot := my_arr[] my_arr:insert(1000) >> my_arr = @[20, 30, 999, 1000] >> snapshot = [20, 30, 999] // Internally, this is implemented using copy-on-write, so it's quite // efficient. // These demos are defined below: demo_keyword_args() demo_structs() demo_enums() demo_lambdas() // Functions must be declared at the top level of a file and must specify the // types of all of their arguments and return value (if any): func add(x:Int, y:Int)->Int: return x + y // Default values for arguments can be provided in place of a type (the type is // inferred from the default value): func show_both(first:Int, second=0)->Text: return "first={first} second={second}" func demo_keyword_args(): >> show_both(1, 2) = "first=1 second=2" // If unspecified, the default argument is used: >> show_both(1) = "first=1 second=0" // Arguments can be specified by name, in any order: >> show_both(second=20, 10) = "first=10 second=20" // Here are some different type signatures: func takes_many_types( boolean:Bool, integer:Int, floating_point_number:Num, text_aka_string:Text, array_of_ints:[Int], table_of_text_to_bools:{Text:Bool}, pointer_to_mutable_array_of_ints:@[Int], maybe_null_pointer_to_int:@Int?, function_from_int_to_text:func(x:Int)->Text, ): pass // Now let's define our own datastructure, a humble struct: struct Person(name:Text, age:Int): // We can define constants here if we want to: max_age := 122 // Methods are defined here as well: func say_age(self:Person): say("My age is {self.age}") // If you want to mutate a value, you must have a mutable pointer: func increase_age(self:@Person, amount=1): self.age += amount // Methods don't have to take a Person as their first argument: func get_cool_name()->Text: return "Blade" func demo_structs(): // Creating a struct: alice := Person("Alice", 30) >> alice = Person(name="Alice", age=30) // Accessing fields: >> alice.age = 30 // Calling methods: alice:say_age() // You can call static methods by using the class name and ".": >> Person.get_cool_name() = "Blade" // Comparisons, conversion to text, and hashing are all handled // automatically when you create a struct: bob := Person("Bob", 30) >> alice == bob = no >> "{alice}" == 'Person(name="Alice", age=30)' = yes table := {alice: "first", bob: "second"} >> table[alice] = "first" // Now let's look at another feature: enums. Tomo enums are tagged unions, also // known as "sum types". You enumerate all the different types of values // something could have, and it's stored internally as a small integer that // indicates which type it is, and any data you want to associate with it. enum Shape( Point, Circle(radius:Num), Rectangle(width:Num, height:Num), ): // Just like with structs, you define methods and constants inside a level // of indentation: func get_area(self:Shape)->Num: // In order to work with an enum, it's most often handy to use a 'when' // statement to get the internal values: when self is Point: return 0 is Circle(r): return Num.PI * r^2 is Rectangle(w, h): return w * h // 'when' statements are checked for exhaustiveness, so the compiler // will give an error if you forgot any cases. You can also use 'else:' // if you want a fallback to handle other cases. func demo_enums(): // Enums are constructed like this: my_shape := Shape.Circle(1.0) // If an enum type doesn't have any associated data, it is not invoked as a // function, but is just a static value: other_shape := Shape.Point // Similar to structs, enums automatically define comparisons, conversion // to text, and hashing: >> my_shape == other_shape = no >> "{my_shape}" == "Shape.Circle(radius=1)" = yes >> {my_shape:"nice"} = {Shape.Circle(radius=1):"nice"} func demo_lambdas(): // Lambdas, or anonymous functions, can be used like this: add_one := func(x:Int): x + 1 >> add_one(5) = 6 // Lambdas can capture closure values, but only as a snapshot from when the // lambda was created: n := 10 add_n := func(x:Int): x + n >> add_n(5) = 15 // The lambda's closure won't change when this variable is reassigned: n = -999 >> add_n(5) = 15