Syntax change: table types are now: {K=V; default=...} and tables

use `{:K=V, ...; default=...}`
This commit is contained in:
Bruce Hill 2025-04-02 16:14:20 -04:00
parent ecaf34247e
commit 6ec8f20fc5
12 changed files with 103 additions and 62 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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):

View File

@ -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()

View File

@ -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),

View File

@ -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):

View File

@ -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])"},

View File

@ -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;
}
}

View File

@ -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)

View File

@ -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:

View File

@ -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}