diff --git a/docs/arrays.md b/docs/arrays.md index 613d793..e3da6e0 100644 --- a/docs/arrays.md +++ b/docs/arrays.md @@ -235,7 +235,7 @@ variable or dereference a heap pointer, it may trigger copy-on-write behavior. - [`func binary_search(arr: [T], by: func(x,y:&T->Int32) = T.compare -> Int)`](#binary_search) - [`func by(arr: [T], step: Int -> [T])`](#by) - [`func clear(arr: @[T] -> Void)`](#clear) -- [`func counts(arr: [T] -> {T,Int})`](#counts) +- [`func counts(arr: [T] -> {T=Int})`](#counts) - [`func find(arr: [T], target: T -> Int?)`](#find) - [`func first(arr: [T], predicate: func(item:&T -> Bool) -> Int)`](#first) - [`func from(arr: [T], first: Int -> [T])`](#from) @@ -334,7 +334,7 @@ Nothing. Counts the occurrences of each element in the array. ```tomo -func counts(arr: [T] -> {T,Int}) +func counts(arr: [T] -> {T=Int}) ``` - `arr`: The array to count elements in. diff --git a/docs/tables.md b/docs/tables.md index 42af25d..f0045ef 100644 --- a/docs/tables.md +++ b/docs/tables.md @@ -7,21 +7,21 @@ support *all* types as both keys and values. ## Syntax -Tables are written using `{}` curly braces with `:` colons associating key +Tables are written using `{}` curly braces with `=` equals signs associating key expressions with value expressions and commas between entries: ```tomo -table := {"A": 10, "B": 20} +table := {"A"=10, "B"=20} ``` Empty tables must specify the key and value types explicitly: ```tomo -empty := {:Text,Int} +empty := {:Text=Int} ``` For type annotations, a table that maps keys with type `K` to values of type -`V` is written as `{K,V}`. +`V` is written as `{K=V}`. ### Comprehensions @@ -41,9 +41,9 @@ optional value: ```tomo table := {"A"=1, "B"=2} >> table["A"] -= 1 : Int? += 1? >> table["missing"] -= none : Int? += none:Int ``` As with all optional values, you can use the `!` postfix operator to assert @@ -52,10 +52,10 @@ use the `or` operator to provide a fallback value in the case that it's none: ```tomo >> table["A"]! -= 1 : Int += 1 >> table["missing"] or -1 -= -1 : Int += -1 ``` ### Fallback Tables @@ -67,7 +67,7 @@ is not found in the table itself: t := {"A"=10} t2 := {"B"=20; fallback=t} >> t2["A"] -= 10 : Int? += 10? ``` The fallback is available by the `.fallback` field, which returns an optional @@ -75,11 +75,30 @@ table value: ```tomo >> t2.fallback -= {"A"=10} : {Text,Int}? += {"A"=10}? >> t.fallback -= none : {Text,Int}? += none:{Text=Int} ``` +### Default Values + +Tables can specify a default value which will be returned if a value is not +present in the table or its fallback (if any). + +```tomo +counts := &{"foo"=12; default=0} +>> counts["foo"] += 12 +>> counts["baz"] += 0 +counts["baz"] += 1 +>> counts["baz"] += 1 +``` + +When values are accessed from a table with a default value, the return type +is non-optional (because a value will always be present). + ## Setting Values You can assign a new key/value mapping or overwrite an existing one using @@ -133,19 +152,19 @@ iterating over any of the new values. ## Table Methods -- [`func bump(t:@{K,V}, key: K, amount: Int = 1 -> Void)`](#bump) -- [`func clear(t:@{K,V})`](#clear) -- [`func get(t:{K,V}, key: K -> V?)`](#get) -- [`func has(t:{K,V}, key: K -> Bool)`](#has) -- [`func remove(t:{K,V}, key: K -> Void)`](#remove) -- [`func set(t:{K,V}, key: K, value: V -> Void)`](#set) +- [`func bump(t:&{K=V}, key: K, amount: Int = 1 -> Void)`](#bump) +- [`func clear(t:&{K=V})`](#clear) +- [`func get(t:{K=V}, key: K -> V?)`](#get) +- [`func has(t:{K=V}, key: K -> Bool)`](#has) +- [`func remove(t:{K=V}, key: K -> Void)`](#remove) +- [`func set(t:{K=V}, key: K, value: V -> Void)`](#set) ### `bump` Increments the value associated with a key by a specified amount. If the key is not already in the table, its value will be assumed to be zero. ```tomo -func bump(t:@{K,V}, key: K, amount: Int = 1 -> Void) +func bump(t:&{K=V}, key: K, amount: Int = 1 -> Void) ``` - `t`: The reference to the table. @@ -170,7 +189,7 @@ t:bump("B", 10) Removes all key-value pairs from the table. ```tomo -func clear(t:@{K,V}) +func clear(t:&{K=V}) ``` - `t`: The reference to the table. @@ -186,32 +205,33 @@ Nothing. --- ### `get` -Retrieves the value associated with a key, or returns null if the key is not present. +Retrieves the value associated with a key, or returns `none` if the key is not present. +**Note:** default values for the table are ignored. ```tomo -func get(t:{K,V}, key: K -> V?) +func get(t:{K=V}, key: K -> V?) ``` - `t`: The table. - `key`: The key whose associated value is to be retrieved. **Returns:** -The value associated with the key or null if the key is not found. +The value associated with the key or `none` if the key is not found. **Example:** ```tomo >> t := {"A"=1, "B"=2} >> t:get("A") -= 1 : Int? += 1? >> t:get("????") -= none : Int? += none:Int >> t:get("A")! -= 1 : Int += 1 >> t:get("????") or 0 -= 0 : Int += 0 ``` --- @@ -220,7 +240,7 @@ The value associated with the key or null if the key is not found. Checks if the table contains a specified key. ```tomo -func has(t:{K,V}, key: K -> Bool) +func has(t:{K=V}, key: K -> Bool) ``` - `t`: The table. @@ -243,7 +263,7 @@ func has(t:{K,V}, key: K -> Bool) Removes the key-value pair associated with a specified key. ```tomo -func remove(t:{K,V}, key: K -> Void) +func remove(t:{K=V}, key: K -> Void) ``` - `t`: The reference to the table. @@ -266,7 +286,7 @@ t:remove("A") Sets or updates the value associated with a specified key. ```tomo -func set(t:{K,V}, key: K, value: V -> Void) +func set(t:{K=V}, key: K, value: V -> Void) ``` - `t`: The reference to the table. diff --git a/docs/text.md b/docs/text.md index ca7e399..fd79fdb 100644 --- a/docs/text.md +++ b/docs/text.md @@ -295,7 +295,7 @@ finding the value because the two texts are equivalent under normalization. - [`func starts_with(text: Text, prefix: Text -> Bool)`](#starts_with) - [`func title(text: Text, language: Text = "C" -> Text)`](#title) - [`func to(text: Text, last: Int -> Text)`](#to) -- [`func translate(translations:{Text,Text} -> Text)`](#translate) +- [`func translate(translations:{Text=Text} -> Text)`](#translate) - [`func trim(text: Text, to_trim: Text = " $\t\r\n", left: Bool = yes, right: Bool = yes -> Text)`](#trim) - [`func upper(text: Text, language: Text "C" -> Text)`](#upper) - [`func utf32_codepoints(text: Text -> [Int32])`](#utf32_codepoints) @@ -1065,7 +1065,7 @@ replacement text, so replacement text is not recursively modified. See [`replace()`](#replace) for more information about replacement behavior. ```tomo -func translate(translations:{Text,Text} -> Text) +func translate(translations:{Text=Text} -> Text) ``` - `text`: The text in which to perform replacements. diff --git a/examples/commands/commands.tm b/examples/commands/commands.tm index 26b8279..cbf5439 100644 --- a/examples/commands/commands.tm +++ b/examples/commands/commands.tm @@ -3,8 +3,8 @@ use ./commands.c use -lunistring -extern run_command:func(exe:Text, args:[Text], env:{Text,Text}, input:[Byte]?, output:&[Byte]?, error:&[Byte]? -> Int32) -extern command_by_line:func(exe:Text, args:[Text], env:{Text,Text} -> func(->Text?)?) +extern run_command:func(exe:Text, args:[Text], env:{Text=Text}, input:[Byte]?, output:&[Byte]?, error:&[Byte]? -> Int32) +extern command_by_line:func(exe:Text, args:[Text], env:{Text=Text} -> func(->Text?)?) enum ExitType(Exited(status:Int32), Signaled(signal:Int32), Failed): func succeeded(e:ExitType -> Bool): @@ -46,8 +46,8 @@ struct ProgramResult(stdout:[Byte], stderr:[Byte], exit_type:ExitType): else: return no -struct Command(command:Text, args=[:Text], env={:Text,Text}): - func from_path(path:Path, args=[:Text], env={:Text,Text} -> Command): +struct Command(command:Text, args=[:Text], env={:Text=Text}): + func from_path(path:Path, args=[:Text], env={:Text=Text} -> Command): return Command(Text(path), args, env) func result(command:Command, input="", input_bytes=[:Byte] -> ProgramResult): diff --git a/examples/ini/ini.tm b/examples/ini/ini.tm index 1c90b71..1e8e015 100644 --- a/examples/ini/ini.tm +++ b/examples/ini/ini.tm @@ -9,10 +9,10 @@ _HELP := " $_USAGE " -func parse_ini(path:Path -> {Text,{Text,Text}}): +func parse_ini(path:Path -> {Text={Text=Text}}): text := path:read() or exit("Could not read INI file: $\[31;1]$(path)$\[]") - sections := @{:Text,@{Text,Text}} - current_section := @{:Text,Text} + sections := @{:Text=@{Text=Text}} + current_section := @{:Text=Text} # Line wraps: text = text:replace_pattern($Pat/\{1 nl}{0+space}/, " ") @@ -22,7 +22,7 @@ func parse_ini(path:Path -> {Text,{Text,Text}}): skip if line:starts_with(";") or line:starts_with("#") if line:matches_pattern($Pat/[?]/): section_name := line:replace($Pat/[?]/, "\1"):trim():lower() - current_section = @{:Text,Text} + current_section = @{:Text=Text} sections[section_name] = current_section else if line:matches_pattern($Pat/{..}={..}/): key := line:replace_pattern($Pat/{..}={..}/, "\1"):trim():lower() diff --git a/examples/learnxiny.tm b/examples/learnxiny.tm index 00c7e94..a394e58 100644 --- a/examples/learnxiny.tm +++ b/examples/learnxiny.tm @@ -123,7 +123,7 @@ func main(): = 0 # Empty tables require specifying the key and value types: - empty_table := {:Text,Int} + empty_table := {:Text=Int} # Tables can be iterated over either by key or key,value: for key in table: @@ -243,7 +243,7 @@ func takes_many_types( floating_point_number:Num, text_aka_string:Text, array_of_ints:[Int], - table_of_text_to_bools:{Text,Bool}, + 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), diff --git a/examples/patterns/patterns.tm b/examples/patterns/patterns.tm index 6afcdc2..1522684 100644 --- a/examples/patterns/patterns.tm +++ b/examples/patterns/patterns.tm @@ -19,7 +19,7 @@ extend Text: func replace_pattern(text:Text, pattern:Pat, replacement:Text, backref="@", recursive=yes -> Text): return inline C : Text { Pattern$replace(_$text, _$pattern, _$replacement, _$backref, _$recursive); } - func translate_patterns(text:Text, replacements:{Pat,Text}, backref="@", recursive=yes -> Text): + func translate_patterns(text:Text, replacements:{Pat=Text}, backref="@", recursive=yes -> Text): return inline C : Text { Pattern$replace_all(_$text, _$replacements, _$backref, _$recursive); } func has_pattern(text:Text, pattern:Pat -> Bool): diff --git a/src/environment.c b/src/environment.c index af697c5..18818a9 100644 --- a/src/environment.c +++ b/src/environment.c @@ -355,7 +355,7 @@ env_t *global_env(void) {"starts_with", "Text$starts_with", "func(text,prefix:Text -> Bool)"}, {"title", "Text$title", "func(text:Text, language='C' -> Text)"}, {"to", "Text$to", "func(text:Text, last:Int -> Text)"}, - {"translate", "Text$translate", "func(text:Text, translations:{Text,Text} -> Text)"}, + {"translate", "Text$translate", "func(text:Text, translations:{Text=Text} -> Text)"}, {"trim", "Text$trim", "func(text:Text, to_trim=\" \t\r\n\", left=yes, right=yes -> Text)"}, {"upper", "Text$upper", "func(text:Text, language='C' -> Text)"}, {"utf32_codepoints", "Text$utf32_codepoints", "func(text:Text -> [Int32])"}, diff --git a/src/parse.c b/src/parse.c index 9757613..a3c6841 100644 --- a/src/parse.c +++ b/src/parse.c @@ -503,14 +503,17 @@ type_ast_t *parse_table_type(parse_ctx_t *ctx, const char *pos) { pos = key_type->end; whitespace(&pos); type_ast_t *value_type = NULL; - ast_t *default_value = NULL; - if (match(&pos, ",")) { + if (match(&pos, "=")) { value_type = expect(ctx, start, &pos, parse_type, "I couldn't parse the rest of this table type"); - } else if (match(&pos, "=")) { - default_value = expect(ctx, start, &pos, parse_extended_expr, "I couldn't parse the rest of this table type"); } else { return NULL; } + spaces(&pos); + ast_t *default_value = NULL; + if (match(&pos, ";") && match_word(&pos, "default")) { + expect_str(ctx, pos, &pos, "=", "I expected an '=' here"); + default_value = expect(ctx, start, &pos, parse_extended_expr, "I couldn't parse the default value for this table"); + } whitespace(&pos); expect_closing(ctx, &pos, "}", "I wasn't able to parse the rest of this table type"); return NewTypeAST(ctx->file, start, pos, TableTypeAST, .key=key_type, .value=value_type, .default_value=default_value); @@ -722,15 +725,12 @@ PARSER(parse_table) { ast_list_t *entries = NULL; type_ast_t *key_type = NULL, *value_type = NULL; - ast_t *default_value = NULL; if (match(&pos, ":")) { whitespace(&pos); key_type = expect(ctx, pos-1, &pos, parse_type, "I couldn't parse a key type for this table"); whitespace(&pos); - if (match(&pos, ",")) { + if (match(&pos, "=")) { value_type = expect(ctx, pos-1, &pos, parse_type, "I couldn't parse the value type for this table"); - } else if (match(&pos, "=")) { - default_value = expect(ctx, pos-1, &pos, parse_extended_expr, "I couldn't parse the default value for this table"); } else { return NULL; } @@ -764,7 +764,7 @@ PARSER(parse_table) { whitespace(&pos); - ast_t *fallback = NULL; + ast_t *fallback = NULL, *default_value = NULL; if (match(&pos, ";")) { for (;;) { whitespace(&pos); @@ -775,11 +775,17 @@ PARSER(parse_table) { if (fallback) parser_err(ctx, attr_start, pos, "This table already has a fallback"); fallback = expect(ctx, attr_start, &pos, parse_expr, "I expected a fallback table"); + } else if (match_word(&pos, "default")) { + whitespace(&pos); + if (!match(&pos, "=")) parser_err(ctx, attr_start, pos, "I expected an '=' after 'default'"); + if (default_value) + parser_err(ctx, attr_start, pos, "This table already has a default"); + default_value = expect(ctx, attr_start, &pos, parse_expr, "I expected a default value"); } else { break; } whitespace(&pos); - if (!match(&pos, ";")) break; + if (!match(&pos, ",")) break; } } diff --git a/test/for.tm b/test/for.tm index e377481..a67e9d5 100644 --- a/test/for.tm +++ b/test/for.tm @@ -15,14 +15,14 @@ func labeled_nums(nums:[Int] -> Text): return "EMPTY" return result -func table_str(t:{Text,Text} -> Text): +func table_str(t:{Text=Text} -> Text): str := "" for k,v in t: str ++= "$k:$v," else: return "EMPTY" return str -func table_key_str(t:{Text,Text} -> Text): +func table_key_str(t:{Text=Text} -> Text): str := "" for k in t: str ++= "$k," @@ -43,7 +43,7 @@ func main(): >> t := {"key1"="value1", "key2"="value2"} >> table_str(t) = "key1:value1,key2:value2," - >> table_str({:Text,Text}) + >> table_str({:Text=Text}) = "EMPTY" >> table_key_str(t) diff --git a/test/serialization.tm b/test/serialization.tm index 6d08552..2027cb9 100644 --- a/test/serialization.tm +++ b/test/serialization.tm @@ -53,7 +53,7 @@ func main(): do: >> obj := {"A"=10, "B"=20; fallback={"C"=30}} >> bytes := obj:serialized() - >> deserialize(bytes -> {Text,Int}) == obj + >> deserialize(bytes -> {Text=Int}) == obj = yes do: diff --git a/test/tables.tm b/test/tables.tm index 9749835..140fd1c 100644 --- a/test/tables.tm +++ b/test/tables.tm @@ -22,7 +22,7 @@ func main(): >> t.length = 2 >> t.fallback - = none : {Text,Int} + = none : {Text=Int} >> t.keys = ["one", "two"] @@ -96,10 +96,25 @@ func main(): >> {1=1, 2=2} <> {2=2, 1=1} = Int32(0) - >> [{:Int,Int}, {0=0}, {99=99}, {1=1, 2=2, 3=3}, {1=1, 99=99, 3=3}, {1=1, 2=-99, 3=3}, {1=1, 99=-99, 3=4}]:sorted() - = [{:Int,Int}, {0=0}, {1=1, 2=-99, 3=3}, {1=1, 2=2, 3=3}, {1=1, 99=99, 3=3}, {1=1, 99=-99, 3=4}, {99=99}] + >> [{:Int=Int}, {0=0}, {99=99}, {1=1, 2=2, 3=3}, {1=1, 99=99, 3=3}, {1=1, 2=-99, 3=3}, {1=1, 99=-99, 3=4}]:sorted() + = [{:Int=Int}, {0=0}, {1=1, 2=-99, 3=3}, {1=1, 2=2, 3=3}, {1=1, 99=99, 3=3}, {1=1, 99=-99, 3=4}, {99=99}] >> [{:Int}, {1}, {2}, {99}, {0, 3}, {1, 2}, {99}]:sorted() = [{:Int}, {0, 3}, {1}, {1, 2}, {2}, {99}, {99}] + do: + # Default values: + counter := &{"x"=10; default=0} + >> counter["x"] + = 10 + >> counter["y"] + = 0 + >> counter:has("x") + = yes + >> counter:has("y") + = no + >> counter["y"] += 1 + >> counter + >> counter + = &{"x"=10, "y"=1; default=0}