Add NULL as a syntax for null values.

This commit is contained in:
Bruce Hill 2024-11-21 13:00:53 -05:00
parent 90573ba7a1
commit f868d02b08
25 changed files with 211 additions and 157 deletions

View File

@ -31,6 +31,7 @@ static CORD compile_int_to_type(env_t *env, ast_t *ast, type_t *target);
static CORD compile_unsigned_type(type_t *t); static CORD compile_unsigned_type(type_t *t);
static CORD promote_to_optional(type_t *t, CORD code); static CORD promote_to_optional(type_t *t, CORD code);
static CORD compile_null(type_t *t); static CORD compile_null(type_t *t);
static CORD compile_to_type(env_t *env, ast_t *ast, type_t *t);
CORD promote_to_optional(type_t *t, CORD code) CORD promote_to_optional(type_t *t, CORD code)
{ {
@ -556,15 +557,7 @@ CORD compile_statement(env_t *env, ast_t *ast)
// Common case: assigning to one variable: // Common case: assigning to one variable:
type_t *lhs_t = get_type(env, assign->targets->ast); type_t *lhs_t = get_type(env, assign->targets->ast);
env_t *val_scope = with_enum_scope(env, lhs_t); env_t *val_scope = with_enum_scope(env, lhs_t);
type_t *rhs_t = get_type(val_scope, assign->values->ast); CORD value = compile_to_type(val_scope, assign->values->ast, lhs_t);
CORD value;
if (lhs_t->tag == IntType && assign->values->ast->tag == Int) {
value = compile_int_to_type(val_scope, assign->values->ast, lhs_t);
} else {
value = compile_maybe_incref(val_scope, assign->values->ast);
if (!promote(val_scope, &value, rhs_t, lhs_t))
code_err(assign->values->ast, "You cannot assign a %T value to a %T operand", rhs_t, lhs_t);
}
return CORD_asprintf( return CORD_asprintf(
"test((%r), %r, %r, %ld, %ld);", "test((%r), %r, %r, %ld, %ld);",
compile_assignment(env, assign->targets->ast, value), compile_assignment(env, assign->targets->ast, value),
@ -583,15 +576,7 @@ CORD compile_statement(env_t *env, ast_t *ast)
for (ast_list_t *target = assign->targets, *value = assign->values; target && value; target = target->next, value = value->next) { for (ast_list_t *target = assign->targets, *value = assign->values; target && value; target = target->next, value = value->next) {
type_t *target_type = get_type(env, target->ast); type_t *target_type = get_type(env, target->ast);
env_t *val_scope = with_enum_scope(env, target_type); env_t *val_scope = with_enum_scope(env, target_type);
type_t *value_type = get_type(val_scope, value->ast); CORD val_code = compile_to_type(val_scope, value->ast, target_type);
CORD val_code;
if (target_type->tag == IntType && value->ast->tag == Int) {
val_code = compile_int_to_type(val_scope, value->ast, target_type);
} else {
val_code = compile_maybe_incref(val_scope, value->ast);
if (!promote(val_scope, &val_code, value_type, target_type))
code_err(value->ast, "You cannot assign a %T value to a %T operand", value_type, target_type);
}
CORD_appendf(&code, "%r $%ld = %r;\n", compile_type(target_type), i++, val_code); CORD_appendf(&code, "%r $%ld = %r;\n", compile_type(target_type), i++, val_code);
} }
i = 1; i = 1;
@ -655,15 +640,7 @@ CORD compile_statement(env_t *env, ast_t *ast)
if (assign->targets && !assign->targets->next && is_idempotent(assign->targets->ast)) { if (assign->targets && !assign->targets->next && is_idempotent(assign->targets->ast)) {
type_t *lhs_t = get_type(env, assign->targets->ast); type_t *lhs_t = get_type(env, assign->targets->ast);
env_t *val_env = with_enum_scope(env, lhs_t); env_t *val_env = with_enum_scope(env, lhs_t);
type_t *rhs_t = get_type(val_env, assign->values->ast); CORD val = compile_to_type(val_env, assign->values->ast, lhs_t);
CORD val;
if (lhs_t->tag == IntType && assign->values->ast->tag == Int) {
val = compile_int_to_type(val_env, assign->values->ast, lhs_t);
} else {
val = compile_maybe_incref(val_env, assign->values->ast);
if (!promote(val_env, &val, rhs_t, lhs_t))
code_err(assign->values->ast, "You cannot assign a %T value to a %T operand", rhs_t, lhs_t);
}
return CORD_all(compile_assignment(env, assign->targets->ast, val), ";\n"); return CORD_all(compile_assignment(env, assign->targets->ast, val), ";\n");
} }
@ -672,15 +649,7 @@ CORD compile_statement(env_t *env, ast_t *ast)
for (ast_list_t *value = assign->values, *target = assign->targets; value && target; value = value->next, target = target->next) { for (ast_list_t *value = assign->values, *target = assign->targets; value && target; value = value->next, target = target->next) {
type_t *lhs_t = get_type(env, target->ast); type_t *lhs_t = get_type(env, target->ast);
env_t *val_env = with_enum_scope(env, lhs_t); env_t *val_env = with_enum_scope(env, lhs_t);
type_t *rhs_t = get_type(val_env, value->ast); CORD val = compile_to_type(val_env, value->ast, lhs_t);
CORD val;
if (rhs_t->tag == IntType && value->ast->tag == Int) {
val = compile_int_to_type(val_env, value->ast, rhs_t);
} else {
val = compile_maybe_incref(val_env, value->ast);
if (!promote(val_env, &val, rhs_t, lhs_t))
code_err(value->ast, "You cannot assign a %T value to a %T operand", rhs_t, lhs_t);
}
CORD_appendf(&code, "%r $%ld = %r;\n", compile_type(lhs_t), i++, val); CORD_appendf(&code, "%r $%ld = %r;\n", compile_type(lhs_t), i++, val);
} }
i = 1; i = 1;
@ -1062,17 +1031,7 @@ CORD compile_statement(env_t *env, ast_t *ast)
code_err(ast, "This function is not supposed to return any values, according to its type signature"); code_err(ast, "This function is not supposed to return any values, according to its type signature");
env = with_enum_scope(env, env->fn_ctx->return_type); env = with_enum_scope(env, env->fn_ctx->return_type);
CORD value; CORD value = compile_to_type(env, ret, env->fn_ctx->return_type);
if (env->fn_ctx->return_type->tag == IntType && ret->tag == Int) {
value = compile_int_to_type(env, ret, env->fn_ctx->return_type);
} else {
type_t *ret_value_t = get_type(env, ret);
value = compile(env, ret);
if (!promote(env, &value, ret_value_t, env->fn_ctx->return_type))
code_err(ast, "This function expects a return value of type %T, but this return has type %T",
env->fn_ctx->return_type, ret_value_t);
}
if (env->deferred) { if (env->deferred) {
code = CORD_all(compile_declaration(env->fn_ctx->return_type, "ret"), " = ", value, ";\n", code); code = CORD_all(compile_declaration(env->fn_ctx->return_type, "ret"), " = ", value, ";\n", code);
value = "ret"; value = "ret";
@ -1580,10 +1539,12 @@ CORD compile_to_pointer_depth(env_t *env, ast_t *ast, int64_t target_depth, bool
return val; return val;
} }
static CORD compile_to_type(env_t *env, ast_t *ast, type_t *t) CORD compile_to_type(env_t *env, ast_t *ast, type_t *t)
{ {
if (ast->tag == Int && is_numeric_type(t)) if (ast->tag == Int && is_numeric_type(t))
return compile_int_to_type(env, ast, t); return compile_int_to_type(env, ast, t);
if (ast->tag == Null && Match(ast, Null)->type == NULL)
return compile_null(t);
CORD code = compile(env, ast); CORD code = compile(env, ast);
type_t *actual = get_type(env, ast); type_t *actual = get_type(env, ast);
if (!promote(env, &code, actual, t)) if (!promote(env, &code, actual, t))
@ -1956,6 +1917,8 @@ CORD compile(env_t *env, ast_t *ast)
{ {
switch (ast->tag) { switch (ast->tag) {
case Null: { case Null: {
if (!Match(ast, Null)->type)
code_err(ast, "This 'NULL' needs to specify what type it is using `!Type` syntax");
type_t *t = parse_type_ast(env, Match(ast, Null)->type); type_t *t = parse_type_ast(env, Match(ast, Null)->type);
return compile_null(t); return compile_null(t);
} }

View File

@ -363,10 +363,10 @@ The index of the first occurrence or `!Int` if not found.
**Example:** **Example:**
```tomo ```tomo
>> [10, 20, 30, 40, 50]:find(20) >> [10, 20, 30, 40, 50]:find(20)
= 2? = 2 : Int?
>> [10, 20, 30, 40, 50]:find(9999) >> [10, 20, 30, 40, 50]:find(9999)
= !Int = NULL : Int?
``` ```
--- ---
@ -394,9 +394,9 @@ item matches.
**Example:** **Example:**
```tomo ```tomo
>> [4, 5, 6]:find(func(i:&Int): i:is_prime()) >> [4, 5, 6]:find(func(i:&Int): i:is_prime())
= 5? = 5 : Int?
>> [4, 6, 8]:find(func(i:&Int): i:is_prime()) >> [4, 6, 8]:find(func(i:&Int): i:is_prime())
= !Int = NULL : Int?
``` ```
--- ---

View File

@ -29,9 +29,9 @@ func parse(text: Text -> Bool?)
**Example:** **Example:**
```tomo ```tomo
>> Bool.parse("yes") >> Bool.parse("yes")
= yes? = yes : Bool?
>> Bool.parse("no") >> Bool.parse("no")
= no? = no : Bool?
>> Bool.parse("???") >> Bool.parse("???")
= !Bool = NULL : Bool?
``` ```

View File

@ -171,22 +171,22 @@ func parse(text: Text -> Int?)
**Returns:** **Returns:**
The integer represented by the text. If the given text contains a value outside The integer represented by the text. If the given text contains a value outside
of the representable range or if the entire text can't be parsed as an integer, of the representable range or if the entire text can't be parsed as an integer,
a null value will be returned. `NULL` will be returned.
**Example:** **Example:**
```tomo ```tomo
>> Int.parse("123") >> Int.parse("123")
= 123? = 123 : Int?
>> Int.parse("0xFF") >> Int.parse("0xFF")
= 255? = 255 : Int?
# Can't parse: # Can't parse:
>> Int.parse("asdf") >> Int.parse("asdf")
= !Int = NULL : Int?
# Outside valid range: # Outside valid range:
>> Int8.parse("9999999") >> Int8.parse("9999999")
= !Int = NULL : Int8?
``` ```
--- ---

View File

@ -16,13 +16,13 @@ successively gets one line from a file at a time until the file is exhausted:
>> iter := (./test.txt):each_line() >> iter := (./test.txt):each_line()
>> iter() >> iter()
= "line one"? = "line one" : Text?
>> iter() >> iter()
= "line two"? = "line two" : Text?
>> iter() >> iter()
= "line three"? = "line three" : Text?
>> iter() >> iter()
= !Text = NULL : Text?
for line in (./test.txt):each_line(): for line in (./test.txt):each_line():
pass pass

View File

@ -523,7 +523,7 @@ Returns a `Moment` object representing the current date and time.
**Description:** **Description:**
Return a new `Moment` object parsed from the given string in the given format, Return a new `Moment` object parsed from the given string in the given format,
or a null value if the value could not be successfully parsed. or `NULL` if the value could not be successfully parsed.
**Signature:** **Signature:**
```tomo ```tomo
@ -539,7 +539,7 @@ func parse(text: Text, format: Text = "%Y-%m-%dT%H:%M:%S%z" -> Moment?)
**Returns:** **Returns:**
If the text was successfully parsed according to the given format, return a If the text was successfully parsed according to the given format, return a
`Moment` representing that information. Otherwise, return a null value. `Moment` representing that information. Otherwise, return `NULL`.
**Example:** **Example:**
```tomo ```tomo

View File

@ -583,7 +583,8 @@ func parse(text: Text -> Num?)
- `text`: The text containing the number. - `text`: The text containing the number.
**Returns:** **Returns:**
The number represented by the text or a null value if the entire text can't be parsed as a number. The number represented by the text or `NULL` if the entire text can't be parsed
as a number.
**Example:** **Example:**
```tomo ```tomo

View File

@ -29,6 +29,74 @@ This establishes a common language for talking about optional values without
having to use a more generalized form of `enum` which may have different naming having to use a more generalized form of `enum` which may have different naming
conventions and which would generate a lot of unnecessary code. conventions and which would generate a lot of unnecessary code.
## Syntax
Optional types are written using a `?` after the type name. So, an optional
integer would be written as `Int?` and an optional array of texts would be
written as `[Text]?`.
Null values can be written explicitly using the `!` prefix operator and the
type of null value. For example, if you wanted to declare a variable that could
be either an integer value or a null value and initialize it as a null value,
you would write it as:
```tomo
x := !Int
```
Similarly, if you wanted to declare a variable that could be an array of texts
or null and initialize it as null, you would write:
```tomo
x := ![Text]
```
If you want to declare a variable and initialize it with a non-null value, but
keep open the possibility of assigning a null value later, you can use the
postfix `?` operator to indicate that a value is optional:
```tomo
x := 5?
# Later on, assign null:
x = !Int
```
## Type Inference
For convenience, null values can also be written as `NULL` for any type in
situations where the compiler knows what type of optional value is expected:
- When assigning to a variable that has already been declared as optional.
- When returning from a function with an explicit optional return type.
- When passing an argument to a function with an optional argument type.
Here are some examples:
```tomo
x := 5?
x = NULL
func doop(arg:Int?)->Text?:
return NULL
doop(NULL)
```
Non-null values can also be automatically promoted to optional values without
the need for an explicit `?` operator in the cases listed above:
```tomo
x := !Int
x = 5
func doop(arg:Int?)->Text?:
return "okay"
doop(123)
```
## Null Checking
In addition to using conditionals to check for null values, you can also use In addition to using conditionals to check for null values, you can also use
`or` to get a non-null value by either providing an alternative non-null value `or` to get a non-null value by either providing an alternative non-null value
or by providing an early out statement like `return`/`skip`/`stop` or a function or by providing an early out statement like `return`/`skip`/`stop` or a function
@ -55,3 +123,18 @@ for line in lines:
# The `or skip` above means that if we're here, `matches` is non-null: # The `or skip` above means that if we're here, `matches` is non-null:
do_stuff(matches) do_stuff(matches)
``` ```
## Implementation Notes
The implementation of optional types is highly efficient and has no memory
overhead for pointers, collection types (arrays, sets, tables, channels),
booleans, texts, enums, nums, or integers (`Int` type only). This is done by
using carefully chosen values, such as `0` for pointers, `2` for booleans, or a
negative length for arrays. However, for fixed-size integers (`Int64`, `Int32`,
`Int16`, and `Int8`), bytes, and structs, an additional byte is required for
out-of-band information about whether the value is null or not.
Floating point numbers (`Num` and `Num32`) use `NaN` to represent null, so
optional nums should be careful to avoid using `NaN` as a non-null value. This
option was chosen to minimize the memory overhead of optional nums and because
`NaN` literally means "not a number".

View File

@ -492,10 +492,10 @@ raised.
**Example:** **Example:**
```tomo ```tomo
>> (./hello.txt):read() >> (./hello.txt):read()
= "Hello"? = "Hello" : Text?
>> (./nosuchfile.xxx):read() >> (./nosuchfile.xxx):read()
= !Text = NULL : Text?
``` ```
--- ---
@ -521,10 +521,10 @@ returned.
**Example:** **Example:**
```tomo ```tomo
>> (./hello.txt):read() >> (./hello.txt):read()
= [72[B], 101[B], 108[B], 108[B], 111[B]]? = [72[B], 101[B], 108[B], 108[B], 111[B]] : [Byte]?
>> (./nosuchfile.xxx):read() >> (./nosuchfile.xxx):read()
= ![Byte] = NULL : [Byte]?
``` ```
--- ---

View File

@ -113,7 +113,7 @@ inside of any datastructures as elements or members.
```tomo ```tomo
nums := @[10, 20, 30] nums := @[10, 20, 30]
>> nums:first(func(x:&Int): x / 2 == 10) >> nums:first(func(x:&Int): x / 2 == 10)
= 2? = 2 : Int?
``` ```
Normal `@` pointers can be promoted to immutable view pointers automatically, Normal `@` pointers can be promoted to immutable view pointers automatically,

View File

@ -8,7 +8,7 @@ infix operator followed by a colon, followed by a collection:
nums := [10, 20, 30] nums := [10, 20, 30]
sum := (+: nums) sum := (+: nums)
>> sum >> sum
= 60? = 60 : Int?
``` ```
Reductions return an optional value which will be a null value if the thing Reductions return an optional value which will be a null value if the thing
@ -22,7 +22,7 @@ nums := [:Int]
sum := (+: nums) sum := (+: nums)
>> sum >> sum
= !Int = NULL : Int?
>> sum or 0 >> sum or 0
= 0 = 0

View File

@ -48,9 +48,9 @@ was found or error if it wasn't:
```tomo ```tomo
>> t := {"x":1, "y":2} >> t := {"x":1, "y":2}
>> t:get("x") >> t:get("x")
= 1? = 1 : Int?
>> t:get("????") >> t:get("????")
= !Int = NULL : Int?
>> t:get("x")! >> t:get("x")!
= 1 = 1
``` ```
@ -187,10 +187,10 @@ The value associated with the key or null if the key is not found.
```tomo ```tomo
>> t := {"A":1, "B":2} >> t := {"A":1, "B":2}
>> t:get("A") >> t:get("A")
= 1? : Int? = 1 : Int?
>> t:get("????") >> t:get("????")
= !Int : Int? = NULL : Int?
>> t:get("A")! >> t:get("A")!
= 1 : Int = 1 : Int

View File

@ -693,13 +693,13 @@ containing information about the match.
**Example:** **Example:**
```tomo ```tomo
>> " #one #two #three ":find($/#{id}/, start=-999) >> " #one #two #three ":find($/#{id}/, start=-999)
= !Match = NULL : Match?
>> " #one #two #three ":find($/#{id}/, start=999) >> " #one #two #three ":find($/#{id}/, start=999)
= !Match = NULL : Match?
>> " #one #two #three ":find($/#{id}/) >> " #one #two #three ":find($/#{id}/)
= Match(text="#one", index=2, captures=["one"])? = Match(text="#one", index=2, captures=["one"]) : Match?
>> " #one #two #three ":find("{id}", start=6) >> " #one #two #three ":find("{id}", start=6)
= Match(text="#two", index=9, captures=["two"])? = Match(text="#two", index=9, captures=["two"]) : Match?
``` ```
--- ---
@ -885,10 +885,10 @@ or a null value otherwise.
**Example:** **Example:**
```tomo ```tomo
>> "hello world":matches($/{id}/) >> "hello world":matches($/{id}/)
= ![Text] = NULL : [Text]?
>> "hello world":matches($/{id} {id}/) >> "hello world":matches($/{id} {id}/)
= ["hello", "world"]? = ["hello", "world"] : [Text]?
``` ```
--- ---

View File

@ -106,13 +106,13 @@ func main():
# Tables are efficient hash maps # Tables are efficient hash maps
table := {"one": 1, "two": 2} table := {"one": 1, "two": 2}
>> table:get("two") >> table:get("two")
= 2? = 2 : Int?
# The value returned is optional (because the key might not be in the table). # 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 # Optional values can be converted to regular values using `!` (which will
# create a runtime error if the value is null) or the `or` operator: # create a runtime error if the value is null) or the `or` operator:
>> table:get("two")! >> table:get("two")!
= 2 = 2 : Int
>> table:get("xxx") or 0 >> table:get("xxx") or 0
= 0 = 0

View File

@ -1532,6 +1532,8 @@ PARSER(parse_lambda) {
PARSER(parse_null) { PARSER(parse_null) {
const char *start = pos; const char *start = pos;
if (match_word(&pos, "NULL"))
return NewAST(ctx->file, start, pos, Null, .type=NULL);
if (!match(&pos, "!")) return NULL; if (!match(&pos, "!")) return NULL;
type_ast_t *type = parse_type(ctx, pos); type_ast_t *type = parse_type(ctx, pos);
if (!type) return NULL; if (!type) return NULL;

View File

@ -64,18 +64,14 @@ public PUREFUNC bool is_null(const void *obj, const TypeInfo_t *non_optional_typ
} }
} }
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wstack-protector"
public Text_t Optional$as_text(const void *obj, bool colorize, const TypeInfo_t *type) public Text_t Optional$as_text(const void *obj, bool colorize, const TypeInfo_t *type)
{ {
if (!obj) if (!obj)
return Text$concat(generic_as_text(obj, colorize, type->OptionalInfo.type), Text("?")); return Text$concat(generic_as_text(obj, colorize, type->OptionalInfo.type), Text("?"));
if (is_null(obj, type->OptionalInfo.type)) if (is_null(obj, type->OptionalInfo.type))
return Text$concat(colorize ? Text("\x1b[31m!") : Text("!"), generic_as_text(NULL, false, type->OptionalInfo.type), return colorize ? Text("\x1b[31mNULL\x1b[m") : Text("NULL");
colorize ? Text("\x1b[m") : Text("")); return generic_as_text(obj, colorize, type->OptionalInfo.type);
return Text$concat(generic_as_text(obj, colorize, type->OptionalInfo.type), colorize ? Text("\x1b[33m?\x1b[m") : Text("?"));
} }
#pragma GCC diagnostic pop
// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1 // vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1

View File

@ -164,11 +164,11 @@ func main():
= [1, 2, 3, 4, 5] = [1, 2, 3, 4, 5]
>> ["a", "b", "c"]:find("b") >> ["a", "b", "c"]:find("b")
= 2? = 2 : Int?
>> ["a", "b", "c"]:find("XXX") >> ["a", "b", "c"]:find("XXX")
= !Int = NULL : Int?
>> [10, 20]:first(func(i:&Int): i:is_prime()) >> [10, 20]:first(func(i:&Int): i:is_prime())
= !Int = NULL : Int?
>> [4, 5, 6]:first(func(i:&Int): i:is_prime()) >> [4, 5, 6]:first(func(i:&Int): i:is_prime())
= 2? = 2 : Int?

View File

@ -4,7 +4,7 @@ struct Struct(x:Int, y:Text):
if should_i: if should_i:
return Struct(123, "hello") return Struct(123, "hello")
else: else:
return !Struct return NULL
enum Enum(X, Y(y:Int)): enum Enum(X, Y(y:Int)):
func maybe(should_i:Bool->Enum?): func maybe(should_i:Bool->Enum?):
@ -75,13 +75,13 @@ func maybe_thread(should_i:Bool->Thread?):
func main(): func main():
>> 5? >> 5?
= 5? : Int? = 5 : Int?
>> if no: >> if no:
!Int !Int
else: else:
5 5
= 5? : Int? = 5 : Int?
>> 5? or -1 >> 5? or -1
= 5 : Int = 5 : Int
@ -98,9 +98,9 @@ func main():
do: do:
!! Ints: !! Ints:
>> yep := maybe_int(yes) >> yep := maybe_int(yes)
= 123? = 123 : Int?
>> nope := maybe_int(no) >> nope := maybe_int(no)
= !Int = NULL : Int?
>> if yep: >> if yep:
>> yep >> yep
= 123 = 123
@ -113,9 +113,9 @@ func main():
!! ... !! ...
!! Int64s: !! Int64s:
>> yep := maybe_int64(yes) >> yep := maybe_int64(yes)
= 123? = 123 : Int64?
>> nope := maybe_int64(no) >> nope := maybe_int64(no)
= !Int64 = NULL : Int64?
>> if yep: >> if yep:
>> yep >> yep
= 123 = 123
@ -128,9 +128,9 @@ func main():
!! ... !! ...
!! Arrays: !! Arrays:
>> yep := maybe_array(yes) >> yep := maybe_array(yes)
= [10, 20, 30]? = [10, 20, 30] : [Int]?
>> nope := maybe_array(no) >> nope := maybe_array(no)
= ![Int] = NULL : [Int]?
>> if yep: >> if yep:
>> yep >> yep
= [10, 20, 30] = [10, 20, 30]
@ -143,9 +143,9 @@ func main():
!! ... !! ...
!! Bools: !! Bools:
>> yep := maybe_bool(yes) >> yep := maybe_bool(yes)
= no? = no : Bool?
>> nope := maybe_bool(no) >> nope := maybe_bool(no)
= !Bool = NULL : Bool?
>> if yep: >> if yep:
>> yep >> yep
= no = no
@ -158,9 +158,9 @@ func main():
!! ... !! ...
!! Text: !! Text:
>> yep := maybe_text(yes) >> yep := maybe_text(yes)
= "Hello"? = "Hello" : Text?
>> nope := maybe_text(no) >> nope := maybe_text(no)
= !Text = NULL : Text?
>> if yep: >> if yep:
>> yep >> yep
= "Hello" = "Hello"
@ -173,9 +173,9 @@ func main():
!! ... !! ...
!! Nums: !! Nums:
>> yep := maybe_num(yes) >> yep := maybe_num(yes)
= 12.3? = 12.3 : Num?
>> nope := maybe_num(no) >> nope := maybe_num(no)
= !Num = NULL : Num?
>> if yep: >> if yep:
>> yep >> yep
= 12.3 = 12.3
@ -188,9 +188,9 @@ func main():
!! ... !! ...
!! Lambdas: !! Lambdas:
>> yep := maybe_lambda(yes) >> yep := maybe_lambda(yes)
= func() [optionals.tm:54]? = func() [optionals.tm:54] : func()?
>> nope := maybe_lambda(no) >> nope := maybe_lambda(no)
= !func() = NULL : func()?
>> if yep: >> if yep:
>> yep >> yep
= func() [optionals.tm:54] = func() [optionals.tm:54]
@ -203,9 +203,9 @@ func main():
!! ... !! ...
!! Structs: !! Structs:
>> yep := Struct.maybe(yes) >> yep := Struct.maybe(yes)
= Struct(x=123, y="hello")? = Struct(x=123, y="hello") : Struct?
>> nope := Struct.maybe(no) >> nope := Struct.maybe(no)
= !Struct = NULL : Struct?
>> if yep: >> if yep:
>> yep >> yep
= Struct(x=123, y="hello") = Struct(x=123, y="hello")
@ -218,9 +218,9 @@ func main():
!! ... !! ...
!! Enums: !! Enums:
>> yep := Enum.maybe(yes) >> yep := Enum.maybe(yes)
= Enum.Y(123)? = Enum.Y(123) : Enum?
>> nope := Enum.maybe(no) >> nope := Enum.maybe(no)
= !Enum = NULL : Enum?
>> if yep: >> if yep:
>> yep >> yep
= Enum.Y(123) = Enum.Y(123)
@ -233,9 +233,9 @@ func main():
!! ... !! ...
!! C Strings: !! C Strings:
>> yep := maybe_c_string(yes) >> yep := maybe_c_string(yes)
= CString("hi")? = CString("hi") : CString?
>> nope := maybe_c_string(no) >> nope := maybe_c_string(no)
= !CString = NULL : CString?
>> if yep: >> if yep:
>> yep >> yep
= CString("hi") = CString("hi")
@ -250,7 +250,7 @@ func main():
>> yep := maybe_channel(yes) >> yep := maybe_channel(yes)
# No "=" test here because channels use addresses in the text version # No "=" test here because channels use addresses in the text version
>> nope := maybe_channel(no) >> nope := maybe_channel(no)
= !|:Int| = NULL : |:Int|?
>> if yep: >> yep >> if yep: >> yep
else: fail("Falsey: $yep") else: fail("Falsey: $yep")
>> if nope: >> if nope:
@ -263,7 +263,7 @@ func main():
>> yep := maybe_thread(yes) >> yep := maybe_thread(yes)
# No "=" test here because threads use addresses in the text version # No "=" test here because threads use addresses in the text version
>> nope := maybe_thread(no) >> nope := maybe_thread(no)
= !Thread = NULL : Thread?
>> if yep: >> yep >> if yep: >> yep
else: fail("Falsey: $yep") else: fail("Falsey: $yep")
>> if nope: >> if nope:
@ -284,9 +284,9 @@ func main():
>> (5? == 5?) >> (5? == 5?)
= yes = yes
>> {!Int, !Int} >> {!Int, !Int}
= {!Int} = {NULL}
>> [5?, !Int, !Int, 6?]:sorted() >> [5?, !Int, !Int, 6?]:sorted()
= [!Int, !Int, 5?, 6?] = [NULL, NULL, 5, 6]
do: do:
>> value := if var := 5?: >> value := if var := 5?:
@ -323,4 +323,4 @@ func main():
= yes = yes
>> [Struct(5,"A")?, Struct(6,"B"), Struct(7,"C")] >> [Struct(5,"A")?, Struct(6,"B"), Struct(7,"C")]
= [Struct(x=5, y="A")?, Struct(x=6, y="B")?, Struct(x=7, y="C")?] = [Struct(x=5, y="A"), Struct(x=6, y="B"), Struct(x=7, y="C")]

View File

@ -23,9 +23,9 @@ func main():
>> tmpfile:write("Hello world") >> tmpfile:write("Hello world")
>> tmpfile:append("!") >> tmpfile:append("!")
>> tmpfile:read() >> tmpfile:read()
= "Hello world!"? = "Hello world!" : Text?
>> tmpfile:read_bytes() >> tmpfile:read_bytes()
= [0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x21]? : [Byte]? = [0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x21] : [Byte]?
>> tmpdir:files():has(tmpfile) >> tmpdir:files():has(tmpfile)
= yes = yes
@ -36,9 +36,9 @@ func main():
fail("Couldn't read lines in $tmpfile") fail("Couldn't read lines in $tmpfile")
>> (./does-not-exist.xxx):read() >> (./does-not-exist.xxx):read()
= !Text = NULL : Text?
>> (./does-not-exist.xxx):read_bytes() >> (./does-not-exist.xxx):read_bytes()
= ![Byte] = NULL : [Byte]?
if lines := (./does-not-exist.xxx):by_line(): if lines := (./does-not-exist.xxx):by_line():
fail("I could read lines in a nonexistent file") fail("I could read lines in a nonexistent file")
else: else:
@ -84,7 +84,7 @@ func main():
>> (./foo.txt):ends_with(".txt") >> (./foo.txt):ends_with(".txt")
= yes = yes
>> (./foo.txt):matches($|{..}/foo{..}|) >> (./foo.txt):matches($|{..}/foo{..}|)
= [".", ".txt"]? = [".", ".txt"] : [Text]?
>> (./foo.txt):replace($/.txt/, ".md") >> (./foo.txt):replace($/.txt/, ".md")
= (./foo.md) = (./foo.md)

View File

@ -2,10 +2,10 @@ struct Foo(x,y:Int)
func main(): func main():
>> (+: [10, 20, 30]) >> (+: [10, 20, 30])
= 60? = 60 : Int?
>> (+: [:Int]) >> (+: [:Int])
= !Int = NULL : Int?
>> (+: [10, 20, 30]) or 0 >> (+: [10, 20, 30]) or 0
= 60 = 60
@ -14,10 +14,10 @@ func main():
= 0 = 0
>> (_max_: [3, 5, 2, 1, 4]) >> (_max_: [3, 5, 2, 1, 4])
= 5? = 5 : Int?
>> (_max_:abs(): [1, -10, 5]) >> (_max_:abs(): [1, -10, 5])
= -10? = -10 : Int?
>> (_max_: [Foo(0, 0), Foo(1, 0), Foo(0, 10)])! >> (_max_: [Foo(0, 0), Foo(1, 0), Foo(0, 10)])!
= Foo(x=1, y=0) = Foo(x=1, y=0)
@ -37,7 +37,7 @@ func main():
= yes = yes
>> (<=: [:Int]) >> (<=: [:Int])
= !Bool = NULL : Bool?
>> (<=: [5, 4, 3, 2, 1])! >> (<=: [5, 4, 3, 2, 1])!
= no = no

View File

@ -60,7 +60,7 @@ func test_text():
>> a := @CorecursiveA(b) >> a := @CorecursiveA(b)
>> b.other = a >> b.other = a
>> a >> a
= @CorecursiveA(@CorecursiveB(@~1?)?) = @CorecursiveA(@CorecursiveB(@~1))
func main(): func main():
test_literals() test_literals()

View File

@ -3,11 +3,11 @@ func main():
= {"one":1, "two":2} = {"one":1, "two":2}
>> t:get("one") >> t:get("one")
= 1? = 1 : Int?
>> t:get("two") >> t:get("two")
= 2? = 2 : Int?
>> t:get("???") >> t:get("???")
= !Int = NULL : Int?
>> t:get("one")! >> t:get("one")!
= 1 = 1
>> t:get("???") or -1 >> t:get("???") or -1
@ -22,7 +22,7 @@ func main():
>> t.length >> t.length
= 2 = 2
>> t.fallback >> t.fallback
= !{Text:Int} = NULL : {Text:Int}?
>> t.keys >> t.keys
= ["one", "two"] = ["one", "two"]
@ -33,16 +33,16 @@ func main():
= {"three":3; fallback={"one":1, "two":2}} = {"three":3; fallback={"one":1, "two":2}}
>> t2:get("one") >> t2:get("one")
= 1? = 1 : Int?
>> t2:get("three") >> t2:get("three")
= 3? = 3 : Int?
>> t2:get("???") >> t2:get("???")
= !Int = NULL : Int?
>> t2.length >> t2.length
= 1 = 1
>> t2.fallback >> t2.fallback
= {"one":1, "two":2}? = {"one":1, "two":2} : {Text:Int}?
t2_str := "" t2_str := ""
for k,v in t2: for k,v in t2:

View File

@ -35,7 +35,7 @@ func main():
>> Text.from_bytes([:Byte 0x41, 0x6D, 0xC3, 0xA9, 0x6C, 0x69, 0x65])! >> Text.from_bytes([:Byte 0x41, 0x6D, 0xC3, 0xA9, 0x6C, 0x69, 0x65])!
= "Amélie" = "Amélie"
>> Text.from_bytes([Byte(0xFF)]) >> Text.from_bytes([Byte(0xFF)])
= !Text = NULL : Text?
>> amelie2 := "Am$(\U65\U301)lie" >> amelie2 := "Am$(\U65\U301)lie"
>> amelie2:split() >> amelie2:split()
@ -189,13 +189,13 @@ func main():
!! Test text:find() !! Test text:find()
>> " one two three ":find($/{id}/, start=-999) >> " one two three ":find($/{id}/, start=-999)
= !Match = NULL : Match?
>> " one two three ":find($/{id}/, start=999) >> " one two three ":find($/{id}/, start=999)
= !Match = NULL : Match?
>> " one two three ":find($/{id}/) >> " one two three ":find($/{id}/)
= Match(text="one", index=2, captures=["one"])? = Match(text="one", index=2, captures=["one"]) : Match?
>> " one two three ":find($/{id}/, start=5) >> " one two three ":find($/{id}/, start=5)
= Match(text="two", index=8, captures=["two"])? = Match(text="two", index=8, captures=["two"]) : Match?
!! Test text slicing: !! Test text slicing:
>> "abcdef":slice() >> "abcdef":slice()
@ -222,7 +222,7 @@ func main():
= ["PENGUIN"] = ["PENGUIN"]
>> Text.from_codepoint_names(["not a valid name here buddy"]) >> Text.from_codepoint_names(["not a valid name here buddy"])
= !Text = NULL : Text?
>> "one two; three four":find_all($/; {..}/) >> "one two; three four":find_all($/; {..}/)
= [Match(text="; three four", index=8, captures=["three four"])] = [Match(text="; three four", index=8, captures=["three four"])]
@ -247,13 +247,13 @@ func main():
= " good(x, fn(y), BAD(z), w) " = " good(x, fn(y), BAD(z), w) "
>> "Hello":matches($/{id}/) >> "Hello":matches($/{id}/)
= ["Hello"]? = ["Hello"] : [Text]?
>> "Hello":matches($/{lower}/) >> "Hello":matches($/{lower}/)
= ![Text] = NULL : [Text]?
>> "Hello":matches($/{upper}/) >> "Hello":matches($/{upper}/)
= ![Text] = NULL : [Text]?
>> "Hello...":matches($/{id}/) >> "Hello...":matches($/{id}/)
= ![Text] = NULL : [Text]?
if matches := "hello world":matches($/{id} {id}/): if matches := "hello world":matches($/{id} {id}/):
>> matches >> matches

View File

@ -484,6 +484,8 @@ type_t *get_type(env_t *env, ast_t *ast)
#pragma GCC diagnostic ignored "-Wswitch-default" #pragma GCC diagnostic ignored "-Wswitch-default"
switch (ast->tag) { switch (ast->tag) {
case Null: { case Null: {
if (!Match(ast, Null)->type)
return Type(OptionalType, .type=NULL);
type_t *t = parse_type_ast(env, Match(ast, Null)->type); type_t *t = parse_type_ast(env, Match(ast, Null)->type);
return Type(OptionalType, .type=t); return Type(OptionalType, .type=t);
} }
@ -1061,6 +1063,9 @@ type_t *get_type(env_t *env, ast_t *ast)
if (ret->tag == AbortType) if (ret->tag == AbortType)
ret = Type(VoidType); ret = Type(VoidType);
if (ret->tag == OptionalType && !Match(ret, OptionalType)->type)
code_err(lambda->body, "This function doesn't return a specific optional type");
if (lambda->ret_type) { if (lambda->ret_type) {
type_t *declared = parse_type_ast(env, lambda->ret_type); type_t *declared = parse_type_ast(env, lambda->ret_type);
if (can_promote(ret, declared)) if (can_promote(ret, declared))

10
types.c
View File

@ -117,7 +117,7 @@ bool type_eq(type_t *a, type_t *b)
bool type_is_a(type_t *t, type_t *req) bool type_is_a(type_t *t, type_t *req)
{ {
if (type_eq(t, req)) return true; if (type_eq(t, req)) return true;
if (req->tag == OptionalType) if (req->tag == OptionalType && Match(req, OptionalType)->type)
return type_is_a(t, Match(req, OptionalType)->type); return type_is_a(t, Match(req, OptionalType)->type);
if (t->tag == PointerType && req->tag == PointerType) { if (t->tag == PointerType && req->tag == PointerType) {
auto t_ptr = Match(t, PointerType); auto t_ptr = Match(t, PointerType);
@ -144,10 +144,14 @@ type_t *type_or_type(type_t *a, type_t *b)
{ {
if (!a) return b; if (!a) return b;
if (!b) return a; if (!b) return a;
if (type_is_a(b, a)) return a; if (a->tag == OptionalType && !Match(a, OptionalType)->type)
if (type_is_a(a, b)) return b; return b->tag == OptionalType ? b : Type(OptionalType, b);
if (b->tag == OptionalType && !Match(b, OptionalType)->type)
return a->tag == OptionalType ? a : Type(OptionalType, a);
if (a->tag == ReturnType && b->tag == ReturnType) if (a->tag == ReturnType && b->tag == ReturnType)
return Type(ReturnType, .ret=type_or_type(Match(a, ReturnType)->ret, Match(b, ReturnType)->ret)); return Type(ReturnType, .ret=type_or_type(Match(a, ReturnType)->ret, Match(b, ReturnType)->ret));
if (type_is_a(b, a)) return a;
if (type_is_a(a, b)) return b;
if (a->tag == AbortType || a->tag == ReturnType) return non_optional(b); if (a->tag == AbortType || a->tag == ReturnType) return non_optional(b);
if (b->tag == AbortType || b->tag == ReturnType) return non_optional(a); if (b->tag == AbortType || b->tag == ReturnType) return non_optional(a);
if ((a->tag == IntType || a->tag == NumType) && (b->tag == IntType || b->tag == NumType)) { if ((a->tag == IntType || a->tag == NumType) && (b->tag == IntType || b->tag == NumType)) {