From 94a3714686391321ed0842a808c7895d092cc000 Mon Sep 17 00:00:00 2001 From: Bruce Hill Date: Fri, 13 Sep 2024 00:27:46 -0400 Subject: Move learnxiny to examples --- README.md | 4 +- examples/README.md | 2 + examples/learnxiny.tm | 357 ++++++++++++++++++++++++++++++++++++++++++++++++++ learnxiny.tm | 357 -------------------------------------------------- 4 files changed, 361 insertions(+), 359 deletions(-) create mode 100644 examples/learnxiny.tm delete mode 100644 learnxiny.tm diff --git a/README.md b/README.md index c2787055..358bb8fc 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ $ ./hello "john doe" --shout Hello John Doe!!! ``` -For more examples, see [learnXinY](/learnxiny.tm) which as an overview of many -language features or the other example programs/modules in +For more examples, see [learnXinY](/examples/learnxiny.tm) which as an overview +of many language features or the other example programs/modules in [examples/](examples/). ## Features diff --git a/examples/README.md b/examples/README.md index aee5b95c..df5af833 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,6 +5,8 @@ - [game/](game/): An example game using raylib. - [http.tm](http.tm): An HTTP library to make basic synchronous HTTP requests. - [ini.tm](ini.tm): An INI configuration file reader tool. +- [learnxiny.tm](learnxiny.tm): A quick overview of language features in the + style of [learnxinyminutes.com](https://learnxinyminutes.com/). - [log.tm](log.tm): A logging utility. - [vectors.tm](vectors.tm): A math vector library. - [wrap.tm](wrap.tm): A command-line program to wrap text. diff --git a/examples/learnxiny.tm b/examples/learnxiny.tm new file mode 100644 index 00000000..7feca6c1 --- /dev/null +++ b/examples/learnxiny.tm @@ -0,0 +1,357 @@ +# 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!") + + # You can also use !! as a shorthand: + !! This is the same as using say(), but a bit easier to type + + # Declare a variable with ':=' (the type is inferred to be integer) + my_variable := 123 + + # Assign a new value + my_variable = 99 + + # Floating point numbers are similar, but require a decimal point: + my_num := 2.0 + + # Strings can use interpolation with the dollar sign $: + say("My variable is $my_variable and this is a sum: $(1 + 2)") + + say(" + Multiline strings begin with a " at the end of a line and continue in + an indented region below. + You can have leading spaces after the first line + and they'll be preserved. + + The multiline string won't include a leading or trailing newline. + ") + + # 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] + + # Empty arrays require specifying the type: + empty_array := [:Int] + >> empty_array.length + = 0 + + # 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" + + # Arrays can be created with array comprehensions, which are loops: + >> [x*10 for x in my_numbers] + = [100, 200, 300] + >> [x*10 for x in my_numbers if x != 20] + = [100, 300] + + # Loop control flow uses "skip" and "stop" + for x in my_numbers: + for y in my_numbers: + if x == y: + skip + + # For readability, you can also use postfix conditionals: + skip if x == y + + if x + y == 60: + # Skip or stop can specify a loop variable if you want to + # affect an enclosing loop: + stop x + + # Tables are efficient hash maps + table := {"one": 1, "two": 2} + >> table:get("two") + = 2? + + # The value returned is optional (because the key might not be in the table). + # Optional values can be converted to regular values using `!` (which will + # create a runtime error if the value is null) or :or_else() which uses a + # fallback value if it's null. + >> table:get("two")! + = 2 + + >> table:get("xxx"):or_else(0) + = 0 + + # Empty tables require specifying the key and value types: + empty_table := {:Text:Int} + + # 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 a fallback table that's used as a fallback when the key + # isn't found in the table itself: + table2 := {"three": 3; fallback=table} + >> table2:get("two")! + = 2 + >> table2:get("three")! + = 3 + + # Tables can also be created with comprehension loops: + >> {x:10*x for x in 5} + = {1:10, 2:20, 3:30, 4:40, 5:50} + + # 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: + table3 := {[10, 20]: "one", [30, 40, 50]: "two"} + >> table3:get([10, 20])! + = "one" + + # Sets are similar to tables, but they represent an unordered collection of + # unique values: + set := {10, 20, 30} + >> set:has(20) + = yes + >> set:has(999) + = no + + # You can do some operations on sets: + other_set := {30, 40, 50} + >> set:with(other_set) + = {10, 20, 30, 40, 50} + >> set:without(other_set) + = {10, 20} + >> set:overlap(other_set) + = {30} + + # 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: "(a)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], + optional_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:get(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(1)" + = yes + + >> {my_shape:"nice"} + = {Shape.Circle(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 + diff --git a/learnxiny.tm b/learnxiny.tm deleted file mode 100644 index 7feca6c1..00000000 --- a/learnxiny.tm +++ /dev/null @@ -1,357 +0,0 @@ -# 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!") - - # You can also use !! as a shorthand: - !! This is the same as using say(), but a bit easier to type - - # Declare a variable with ':=' (the type is inferred to be integer) - my_variable := 123 - - # Assign a new value - my_variable = 99 - - # Floating point numbers are similar, but require a decimal point: - my_num := 2.0 - - # Strings can use interpolation with the dollar sign $: - say("My variable is $my_variable and this is a sum: $(1 + 2)") - - say(" - Multiline strings begin with a " at the end of a line and continue in - an indented region below. - You can have leading spaces after the first line - and they'll be preserved. - - The multiline string won't include a leading or trailing newline. - ") - - # 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] - - # Empty arrays require specifying the type: - empty_array := [:Int] - >> empty_array.length - = 0 - - # 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" - - # Arrays can be created with array comprehensions, which are loops: - >> [x*10 for x in my_numbers] - = [100, 200, 300] - >> [x*10 for x in my_numbers if x != 20] - = [100, 300] - - # Loop control flow uses "skip" and "stop" - for x in my_numbers: - for y in my_numbers: - if x == y: - skip - - # For readability, you can also use postfix conditionals: - skip if x == y - - if x + y == 60: - # Skip or stop can specify a loop variable if you want to - # affect an enclosing loop: - stop x - - # Tables are efficient hash maps - table := {"one": 1, "two": 2} - >> table:get("two") - = 2? - - # The value returned is optional (because the key might not be in the table). - # Optional values can be converted to regular values using `!` (which will - # create a runtime error if the value is null) or :or_else() which uses a - # fallback value if it's null. - >> table:get("two")! - = 2 - - >> table:get("xxx"):or_else(0) - = 0 - - # Empty tables require specifying the key and value types: - empty_table := {:Text:Int} - - # 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 a fallback table that's used as a fallback when the key - # isn't found in the table itself: - table2 := {"three": 3; fallback=table} - >> table2:get("two")! - = 2 - >> table2:get("three")! - = 3 - - # Tables can also be created with comprehension loops: - >> {x:10*x for x in 5} - = {1:10, 2:20, 3:30, 4:40, 5:50} - - # 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: - table3 := {[10, 20]: "one", [30, 40, 50]: "two"} - >> table3:get([10, 20])! - = "one" - - # Sets are similar to tables, but they represent an unordered collection of - # unique values: - set := {10, 20, 30} - >> set:has(20) - = yes - >> set:has(999) - = no - - # You can do some operations on sets: - other_set := {30, 40, 50} - >> set:with(other_set) - = {10, 20, 30, 40, 50} - >> set:without(other_set) - = {10, 20} - >> set:overlap(other_set) - = {30} - - # 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: "(a)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], - optional_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:get(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(1)" - = yes - - >> {my_shape:"nice"} - = {Shape.Circle(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 - -- cgit v1.2.3