Commit a3c97081ca

Andrew Kelley <superjoe30@gmail.com>
2016-01-07 11:23:38
add ?? maybe unwrapping binary operator
add null literal fix number literal / maybe interactions
1 parent 9b9fd5a
doc/vim/syntax/zig.vim
@@ -14,6 +14,7 @@ syn keyword zigStatement goto break return continue asm
 syn keyword zigConditional if else match
 syn keyword zigRepeat while for
 
+syn keyword zigConstant null
 syn keyword zigKeyword fn unreachable use void
 syn keyword zigType bool i8 u8 i16 u16 i32 u32 i64 u64 isize usize f32 f64 f128 string
 
@@ -28,6 +29,12 @@ syn match zigHexNumber display "\<0x[a-fA-F0-9_]\+\%([iu]\%(size\|8\|16\|32\|64\
 syn match zigOctNumber display "\<0o[0-7_]\+\%([iu]\%(size\|8\|16\|32\|64\)\)\="
 syn match zigBinNumber display "\<0b[01_]\+\%([iu]\%(size\|8\|16\|32\|64\)\)\="
 
+
+syn match zigCharacterInvalid display contained /b\?'\zs[\n\r\t']\ze'/
+syn match zigCharacterInvalidUnicode display contained /b'\zs[^[:cntrl:][:graph:][:alnum:][:space:]]\ze'/
+syn match zigCharacter /b'\([^\\]\|\\\(.\|x\x\{2}\)\)'/ contains=zigEscape,zigEscapeError,zigCharacterInvalid,zigCharacterInvalidUnicode
+syn match zigCharacter /'\([^\\]\|\\\(.\|x\x\{2}\|u\x\{4}\|U\x\{8}\|u{\x\{1,6}}\)\)'/ contains=zigEscape,zigEscapeUnicode,zigEscapeError,zigCharacterInvalid
+
 syn match zigShebang /\%^#![^[].*/
 
 syn region zigCommentLine start="//" end="$" contains=zigTodo,@Spell
@@ -64,6 +71,9 @@ hi def link zigCommentBlockDoc zigCommentLineDoc
 hi def link zigTodo Todo
 hi def link zigStringContinuation Special
 hi def link zigString String
+hi def link zigCharacterInvalid Error
+hi def link zigCharacterInvalidUnicode zigCharacterInvalid
+hi def link zigCharacter Character
 hi def link zigEscape Special
 hi def link zigEscapeUnicode zigEscape
 hi def link zigEscapeError Error
doc/langref.md
@@ -98,7 +98,9 @@ AsmInputItem : token(LBracket) token(Symbol) token(RBracket) token(String) token
 
 AsmClobbers: token(Colon) list(token(String), token(Comma))
 
-AssignmentExpression : BoolOrExpression AssignmentOperator BoolOrExpression | BoolOrExpression
+UnwrapMaybeExpression : BoolOrExpression token(DoubleQuestion) BoolOrExpression | BoolOrExpression
+
+AssignmentExpression : UnwrapMaybeExpression AssignmentOperator UnwrapMaybeExpression | UnwrapMaybeExpression
 
 AssignmentOperator : token(Eq) | token(TimesEq) | token(DivEq) | token(ModEq) | token(PlusEq) | token(MinusEq) | token(BitShiftLeftEq) | token(BitShiftRightEq) | token(BitAndEq) | token(BitXorEq) | token(BitOrEq) | token(BoolAndEq) | token(BoolOrEq) 
 
@@ -166,7 +168,7 @@ Goto: token(Goto) token(Symbol)
 
 GroupedExpression : token(LParen) Expression token(RParen)
 
-KeywordLiteral : token(Unreachable) | token(Void) | token(True) | token(False)
+KeywordLiteral : token(Unreachable) | token(Void) | token(True) | token(False) | token(Null)
 ```
 
 ## Operator Precedence
@@ -184,6 +186,7 @@ as
 == != < > <= >=
 &&
 ||
+??
 = *= /= %= += -= <<= >>= &= ^= |= &&= ||=
 ```
 
@@ -192,7 +195,7 @@ as
 ### Characters and Strings
 
                 | Example  | Characters  | Escapes        | Null Term | Type
----------------------------------------------------------------------------------
+----------------|----------|-------------|----------------|-----------|----------
  Byte           | 'H'      | All ASCII   | Byte           | No        | u8
  UTF-8 Bytes    | "hello"  | All Unicode | Byte & Unicode | No        | [5; u8]
  UTF-8 C string | c"hello" | All Unicode | Byte & Unicode | Yes       | *const u8
@@ -200,7 +203,7 @@ as
 ### Byte Escapes
 
       | Name
------------------------------------------------
+------|----------------------------------------
  \x7F | 8-bit character code (exactly 2 digits)
  \n   | Newline
  \r   | Carriage return
@@ -213,13 +216,13 @@ as
 ### Unicode Escapes
 
           | Name
-----------------------------------------------------------
+----------|-----------------------------------------------
  \u{7FFF} | 24-bit Unicode character code (up to 6 digits)
 
 ### Numbers
 
  Number literals    | Example     | Exponentiation
---------------------------------------------------
+--------------------|-------------|---------------
  Decimal integer    | 98222       | N/A
  Hex integer        | 0xff        | N/A
  Octal integer      | 0o77        | N/A
example/guess_number/main.zig
@@ -14,7 +14,7 @@ pub fn main(argc: isize, argv: &&u8, env: &&u8) -> i32 {
     var err : isize;
     if ({err = os_get_random_bytes(&seed as &u8, #sizeof(u32)); err != #sizeof(u32)}) {
         // TODO full error message
-        fprint_str(stderr_fileno, "unable to get random bytes");
+        fprint_str(stderr_fileno, "unable to get random bytes\n");
         return 1;
     }
 
@@ -27,11 +27,14 @@ pub fn main(argc: isize, argv: &&u8, env: &&u8) -> i32 {
     print_u64(answer);
     print_str("\n");
 
-    return 0;
-
-    /*
     while (true) {
-        const line = readline("\nGuess a number between 1 and 100: ");
+        print_str("\nGuess a number between 1 and 100: ");
+        var line_buf : [20]u8;
+        const line = readline(line_buf) ?? {
+            // TODO full error message
+            fprint_str(stderr_fileno, "unable to read input\n");
+            return 1;
+        };
 
         if (const guess ?= parse_u64(line)) {
             if (guess > answer) {
@@ -46,5 +49,4 @@ pub fn main(argc: isize, argv: &&u8, env: &&u8) -> i32 {
             print_str("Invalid number format.\n");
         }
     }
-    */
 }
example/maybe_type/main.zig
@@ -15,5 +15,21 @@ pub fn main(argc: isize, argv: &&u8, env: &&u8) -> i32 {
         print_str("x is none\n");
     }
 
+    const next_x : ?i32 = null;
+
+    const z = next_x ?? 1234;
+
+    if (z != 1234) {
+        print_str("BAD\n");
+    }
+
+    const final_x : ?i32 = 13;
+
+    const num = final_x ?? unreachable;
+
+    if (num != 13) {
+        print_str("BAD\n");
+    }
+
     return 0;
 }
src/analyze.cpp
@@ -48,6 +48,7 @@ static AstNode *first_executing_node(AstNode *node) {
         case NodeTypeUse:
         case NodeTypeVoid:
         case NodeTypeBoolLiteral:
+        case NodeTypeNullLiteral:
         case NodeTypeIfBoolExpr:
         case NodeTypeIfVarExpr:
         case NodeTypeLabel:
@@ -358,6 +359,7 @@ static TypeTableEntry *eval_const_expr_bin_op(CodeGen *g, BlockContext *context,
         case BinOpTypeSub:
         case BinOpTypeMult:
         case BinOpTypeDiv:
+        case BinOpTypeUnwrapMaybe:
             return g->builtin_types.entry_invalid;
         case BinOpTypeInvalid:
         case BinOpTypeAssign:
@@ -388,6 +390,8 @@ static TypeTableEntry *eval_const_expr(CodeGen *g, BlockContext *context,
         case NodeTypeBoolLiteral:
             out_number_literal->data.x_uint = node->data.bool_literal ? 1 : 0;
             return node->codegen_node->expr_node.type_entry;
+        case NodeTypeNullLiteral:
+            return node->codegen_node->expr_node.type_entry;
         case NodeTypeBinOpExpr:
             return eval_const_expr_bin_op(g, context, node, out_number_literal);
         case NodeTypeCompilerFnType:
@@ -877,6 +881,7 @@ static void preview_function_declarations(CodeGen *g, ImportTableEntry *import,
         case NodeTypeUnreachable:
         case NodeTypeVoid:
         case NodeTypeBoolLiteral:
+        case NodeTypeNullLiteral:
         case NodeTypeSymbol:
         case NodeTypeCastExpr:
         case NodeTypePrefixOpExpr:
@@ -951,6 +956,7 @@ static void preview_types(CodeGen *g, ImportTableEntry *import, AstNode *node) {
         case NodeTypeUnreachable:
         case NodeTypeVoid:
         case NodeTypeBoolLiteral:
+        case NodeTypeNullLiteral:
         case NodeTypeSymbol:
         case NodeTypeCastExpr:
         case NodeTypePrefixOpExpr:
@@ -1018,7 +1024,7 @@ static bool num_lit_fits_in_other_type(CodeGen *g, TypeTableEntry *literal_type,
                 return false;
             }
         case TypeTableEntryIdMaybe:
-            return num_lit_fits_in_other_type(g, literal_type, other_type->data.maybe.child_type);
+            return false;
     }
     zig_unreachable();
 }
@@ -1133,6 +1139,9 @@ static TypeTableEntry *resolve_type_compatibility(CodeGen *g, BlockContext *cont
     if (actual_type->id == TypeTableEntryIdNumberLiteral &&
         num_lit_fits_in_other_type(g, actual_type, expected_type))
     {
+        assert(!node->codegen_node->data.num_lit_node.resolved_type ||
+                node->codegen_node->data.num_lit_node.resolved_type == expected_type);
+        node->codegen_node->data.num_lit_node.resolved_type = expected_type;
         return expected_type;
     }
 
@@ -1419,6 +1428,7 @@ static bool is_op_allowed(TypeTableEntry *type, BinOpType op) {
         case BinOpTypeMult:
         case BinOpTypeDiv:
         case BinOpTypeMod:
+        case BinOpTypeUnwrapMaybe:
             zig_unreachable();
     }
     zig_unreachable();
@@ -1619,6 +1629,24 @@ static TypeTableEntry *analyze_bin_op_expr(CodeGen *g, ImportTableEntry *import,
 
                 return resolve_peer_type_compatibility(g, context, node, op1, op2, lhs_type, rhs_type);
             }
+        case BinOpTypeUnwrapMaybe:
+            {
+                AstNode *op1 = node->data.bin_op_expr.op1;
+                AstNode *op2 = node->data.bin_op_expr.op2;
+                TypeTableEntry *lhs_type = analyze_expression(g, import, context, nullptr, op1);
+
+                if (lhs_type->id == TypeTableEntryIdInvalid) {
+                    return lhs_type;
+                } else if (lhs_type->id == TypeTableEntryIdMaybe) {
+                    TypeTableEntry *child_type = lhs_type->data.maybe.child_type;
+                    analyze_expression(g, import, context, child_type, op2);
+                    return child_type;
+                } else {
+                    add_node_error(g, op1,
+                        buf_sprintf("expected maybe type, got '%s'",
+                            buf_ptr(&lhs_type->name)));
+                }
+            }
         case BinOpTypeInvalid:
             zig_unreachable();
     }
@@ -1695,6 +1723,27 @@ static VariableTableEntry *analyze_variable_declaration(CodeGen *g, ImportTableE
     return analyze_variable_declaration_raw(g, import, context, node, variable_declaration, false);
 }
 
+static TypeTableEntry *analyze_null_literal_expr(CodeGen *g, ImportTableEntry *import,
+        BlockContext *block_context, TypeTableEntry *expected_type, AstNode *node)
+{
+    assert(node->type == NodeTypeNullLiteral);
+
+    if (expected_type) {
+        assert(expected_type->id == TypeTableEntryIdMaybe);
+
+        assert(node->codegen_node);
+        node->codegen_node->data.struct_val_expr_node.type_entry = expected_type;
+        node->codegen_node->data.struct_val_expr_node.source_node = node;
+        block_context->struct_val_expr_alloca_list.append(&node->codegen_node->data.struct_val_expr_node);
+
+        return expected_type;
+    } else {
+        add_node_error(g, node,
+                buf_sprintf("unable to determine null type"));
+        return g->builtin_types.entry_invalid;
+    }
+}
+
 static TypeTableEntry *analyze_number_literal_expr(CodeGen *g, ImportTableEntry *import,
         BlockContext *block_context, TypeTableEntry *expected_type, AstNode *node)
 {
@@ -1706,8 +1755,11 @@ static TypeTableEntry *analyze_number_literal_expr(CodeGen *g, ImportTableEntry
     } else if (expected_type) {
         NumberLiteralNode *codegen_num_lit = &node->codegen_node->data.num_lit_node;
         assert(!codegen_num_lit->resolved_type);
-        codegen_num_lit->resolved_type = resolve_type_compatibility(g, block_context, node, expected_type, num_lit_type);
-        return codegen_num_lit->resolved_type;
+        TypeTableEntry *after_implicit_cast_resolved_type =
+            resolve_type_compatibility(g, block_context, node, expected_type, num_lit_type);
+        assert(codegen_num_lit->resolved_type ||
+                after_implicit_cast_resolved_type->id == TypeTableEntryIdInvalid);
+        return after_implicit_cast_resolved_type;
     } else {
         return num_lit_type;
     }
@@ -2174,6 +2226,10 @@ static TypeTableEntry * analyze_expression(CodeGen *g, ImportTableEntry *import,
             return_type = g->builtin_types.entry_bool;
             break;
 
+        case NodeTypeNullLiteral:
+            return_type = analyze_null_literal_expr(g, import, context, expected_type, node);
+            break;
+
         case NodeTypeSymbol:
             {
                 return_type = analyze_variable_name(g, import, context, node, &node->data.symbol);
@@ -2491,6 +2547,7 @@ static void analyze_top_level_declaration(CodeGen *g, ImportTableEntry *import,
         case NodeTypeUnreachable:
         case NodeTypeVoid:
         case NodeTypeBoolLiteral:
+        case NodeTypeNullLiteral:
         case NodeTypeSymbol:
         case NodeTypeCastExpr:
         case NodeTypePrefixOpExpr:
src/codegen.cpp
@@ -613,6 +613,7 @@ static LLVMValueRef gen_arithmetic_bin_op(CodeGen *g, AstNode *source_node,
         case BinOpTypeAssign:
         case BinOpTypeAssignBoolAnd:
         case BinOpTypeAssignBoolOr:
+        case BinOpTypeUnwrapMaybe:
             zig_unreachable();
     }
     zig_unreachable();
@@ -814,6 +815,70 @@ static LLVMValueRef gen_assign_expr(CodeGen *g, AstNode *node) {
     return gen_assign_raw(g, node, node->data.bin_op_expr.bin_op, target_ref, value, op1_type, op2_type);
 }
 
+static LLVMValueRef gen_unwrap_maybe(CodeGen *g, AstNode *node, LLVMValueRef maybe_struct_ref) {
+    add_debug_source_node(g, node);
+    LLVMValueRef maybe_field_ptr = LLVMBuildStructGEP(g->builder, maybe_struct_ref, 0, "");
+    // TODO if it's a struct we might not want to load the pointer
+    return LLVMBuildLoad(g->builder, maybe_field_ptr, "");
+}
+
+static LLVMValueRef gen_unwrap_maybe_expr(CodeGen *g, AstNode *node) {
+    assert(node->type == NodeTypeBinOpExpr);
+    assert(node->data.bin_op_expr.bin_op == BinOpTypeUnwrapMaybe);
+
+    AstNode *op1_node = node->data.bin_op_expr.op1;
+    AstNode *op2_node = node->data.bin_op_expr.op2;
+
+    LLVMValueRef maybe_struct_ref = gen_expr(g, op1_node);
+
+    add_debug_source_node(g, node);
+    LLVMValueRef maybe_field_ptr = LLVMBuildStructGEP(g->builder, maybe_struct_ref, 1, "");
+    LLVMValueRef cond_value = LLVMBuildLoad(g->builder, maybe_field_ptr, "");
+
+    LLVMBasicBlockRef non_null_block = LLVMAppendBasicBlock(g->cur_fn->fn_value, "MaybeNonNull");
+    LLVMBasicBlockRef null_block = LLVMAppendBasicBlock(g->cur_fn->fn_value, "MaybeNull");
+    LLVMBasicBlockRef end_block;
+    
+    bool non_null_reachable = get_expr_type(op1_node)->id != TypeTableEntryIdUnreachable;
+    bool null_reachable = get_expr_type(op2_node)->id != TypeTableEntryIdUnreachable;
+    bool end_reachable = non_null_reachable || null_reachable;
+    if (end_reachable) {
+        end_block = LLVMAppendBasicBlock(g->cur_fn->fn_value, "MaybeEnd");
+    }
+
+    LLVMBuildCondBr(g->builder, cond_value, non_null_block, null_block);
+
+    LLVMPositionBuilderAtEnd(g->builder, non_null_block);
+    LLVMValueRef non_null_result = gen_unwrap_maybe(g, op1_node, maybe_struct_ref);
+    if (non_null_reachable) {
+        add_debug_source_node(g, node);
+        LLVMBuildBr(g->builder, end_block);
+    }
+
+    LLVMPositionBuilderAtEnd(g->builder, null_block);
+    LLVMValueRef null_result = gen_expr(g, op2_node);
+    if (null_reachable) {
+        add_debug_source_node(g, node);
+        LLVMBuildBr(g->builder, end_block);
+    }
+
+    if (end_reachable) {
+        LLVMPositionBuilderAtEnd(g->builder, end_block);
+        if (null_reachable) {
+            add_debug_source_node(g, node);
+            LLVMValueRef phi = LLVMBuildPhi(g->builder, LLVMTypeOf(non_null_result), "");
+            LLVMValueRef incoming_values[2] = {non_null_result, null_result};
+            LLVMBasicBlockRef incoming_blocks[2] = {non_null_block, null_block};
+            LLVMAddIncoming(phi, incoming_values, incoming_blocks, 2);
+            return phi;
+        } else {
+            return non_null_result;
+        }
+    }
+
+    return nullptr;
+}
+
 static LLVMValueRef gen_bin_op_expr(CodeGen *g, AstNode *node) {
     switch (node->data.bin_op_expr.bin_op) {
         case BinOpTypeInvalid:
@@ -843,6 +908,8 @@ static LLVMValueRef gen_bin_op_expr(CodeGen *g, AstNode *node) {
         case BinOpTypeCmpLessOrEq:
         case BinOpTypeCmpGreaterOrEq:
             return gen_cmp_expr(g, node);
+        case BinOpTypeUnwrapMaybe:
+            return gen_unwrap_maybe_expr(g, node);
         case BinOpTypeBinOr:
         case BinOpTypeBinXor:
         case BinOpTypeBinAnd:
@@ -1124,6 +1191,22 @@ static LLVMValueRef gen_asm_expr(CodeGen *g, AstNode *node) {
     return LLVMBuildCall(g->builder, asm_fn, param_values, input_and_output_count, "");
 }
 
+static LLVMValueRef gen_null_literal(CodeGen *g, AstNode *node) {
+    assert(node->type == NodeTypeNullLiteral);
+
+    TypeTableEntry *type_entry = get_expr_type(node);
+    assert(type_entry->id == TypeTableEntryIdMaybe);
+
+    LLVMValueRef tmp_struct_ptr = node->codegen_node->data.struct_val_expr_node.ptr;
+
+    add_debug_source_node(g, node);
+    LLVMValueRef field_ptr = LLVMBuildStructGEP(g->builder, tmp_struct_ptr, 1, "");
+    LLVMValueRef null_value = LLVMConstNull(LLVMInt1Type());
+    LLVMBuildStore(g->builder, null_value, field_ptr);
+
+    return tmp_struct_ptr;
+}
+
 static LLVMValueRef gen_struct_val_expr(CodeGen *g, AstNode *node) {
     assert(node->type == NodeTypeStructValueExpr);
 
@@ -1242,10 +1325,7 @@ static LLVMValueRef gen_var_decl_raw(CodeGen *g, AstNode *source_node, AstNodeVa
         LLVMValueRef value;
         if (unwrap_maybe) {
             assert(var_decl->expr);
-            add_debug_source_node(g, source_node);
-            LLVMValueRef maybe_field_ptr = LLVMBuildStructGEP(g->builder, *init_value, 0, "");
-            // TODO if it's a struct we might not want to load the pointer
-            value = LLVMBuildLoad(g->builder, maybe_field_ptr, "");
+            value = gen_unwrap_maybe(g, source_node, *init_value);
         } else {
             value = *init_value;
         }
@@ -1375,6 +1455,8 @@ static LLVMValueRef gen_expr_no_cast(CodeGen *g, AstNode *node) {
                 return LLVMConstAllOnes(LLVMInt1Type());
             else
                 return LLVMConstNull(LLVMInt1Type());
+        case NodeTypeNullLiteral:
+            return gen_null_literal(g, node);
         case NodeTypeIfBoolExpr:
             return gen_if_bool_expr(g, node);
         case NodeTypeIfVarExpr:
src/parser.cpp
@@ -48,6 +48,7 @@ static const char *bin_op_str(BinOpType bin_op) {
         case BinOpTypeAssignBitOr:         return "|=";
         case BinOpTypeAssignBoolAnd:       return "&&=";
         case BinOpTypeAssignBoolOr:        return "||=";
+        case BinOpTypeUnwrapMaybe:         return "??";
     }
     zig_unreachable();
 }
@@ -117,6 +118,8 @@ const char *node_type_str(NodeType node_type) {
             return "Void";
         case NodeTypeBoolLiteral:
             return "BoolLiteral";
+        case NodeTypeNullLiteral:
+            return "NullLiteral";
         case NodeTypeIfBoolExpr:
             return "IfBoolExpr";
         case NodeTypeIfVarExpr:
@@ -349,6 +352,9 @@ void ast_print(AstNode *node, int indent) {
         case NodeTypeBoolLiteral:
             fprintf(stderr, "%s '%s'\n", node_type_str(node->type), node->data.bool_literal ? "true" : "false");
             break;
+        case NodeTypeNullLiteral:
+            fprintf(stderr, "%s\n", node_type_str(node->type));
+            break;
         case NodeTypeIfBoolExpr:
             fprintf(stderr, "%s\n", node_type_str(node->type));
             if (node->data.if_bool_expr.condition)
@@ -1280,6 +1286,7 @@ static AstNode *ast_parse_struct_val_expr(ParseContext *pc, int *token_index) {
 
 /*
 PrimaryExpression : token(Number) | token(String) | token(CharLiteral) | KeywordLiteral | GroupedExpression | Goto | token(Break) | token(Continue) | BlockExpression | token(Symbol) | StructValueExpression | CompilerFnType
+KeywordLiteral : token(Unreachable) | token(Void) | token(True) | token(False) | token(Null)
 */
 static AstNode *ast_parse_primary_expr(ParseContext *pc, int *token_index, bool mandatory) {
     Token *token = &pc->tokens->at(*token_index);
@@ -1317,6 +1324,10 @@ static AstNode *ast_parse_primary_expr(ParseContext *pc, int *token_index, bool
         node->data.bool_literal = false;
         *token_index += 1;
         return node;
+    } else if (token->id == TokenIdKeywordNull) {
+        AstNode *node = ast_create_node(pc, NodeTypeNullLiteral, token);
+        *token_index += 1;
+        return node;
     } else if (token->id == TokenIdSymbol) {
         Token *next_token = &pc->tokens->at(*token_index + 1);
 
@@ -2045,19 +2056,45 @@ static BinOpType ast_parse_ass_op(ParseContext *pc, int *token_index, bool manda
 }
 
 /*
-AssignmentExpression : BoolOrExpression AssignmentOperator BoolOrExpression | BoolOrExpression
+UnwrapMaybeExpression : BoolOrExpression token(DoubleQuestion) BoolOrExpression | BoolOrExpression
 */
-static AstNode *ast_parse_ass_expr(ParseContext *pc, int *token_index, bool mandatory) {
+static AstNode *ast_parse_unwrap_maybe_expr(ParseContext *pc, int *token_index, bool mandatory) {
     AstNode *lhs = ast_parse_bool_or_expr(pc, token_index, mandatory);
     if (!lhs)
         return nullptr;
 
+    Token *token = &pc->tokens->at(*token_index);
+
+    if (token->id != TokenIdDoubleQuestion) {
+        return lhs;
+    }
+
+    *token_index += 1;
+
+    AstNode *rhs = ast_parse_bool_or_expr(pc, token_index, true);
+
+    AstNode *node = ast_create_node(pc, NodeTypeBinOpExpr, token);
+    node->data.bin_op_expr.op1 = lhs;
+    node->data.bin_op_expr.bin_op = BinOpTypeUnwrapMaybe;
+    node->data.bin_op_expr.op2 = rhs;
+
+    return node;
+}
+
+/*
+AssignmentExpression : UnwrapMaybeExpression AssignmentOperator UnwrapMaybeExpression | UnwrapMaybeExpression
+*/
+static AstNode *ast_parse_ass_expr(ParseContext *pc, int *token_index, bool mandatory) {
+    AstNode *lhs = ast_parse_unwrap_maybe_expr(pc, token_index, mandatory);
+    if (!lhs)
+        return nullptr;
+
     Token *token = &pc->tokens->at(*token_index);
     BinOpType ass_op = ast_parse_ass_op(pc, token_index, false);
     if (ass_op == BinOpTypeInvalid)
         return lhs;
 
-    AstNode *rhs = ast_parse_bool_or_expr(pc, token_index, true);
+    AstNode *rhs = ast_parse_unwrap_maybe_expr(pc, token_index, true);
 
     AstNode *node = ast_create_node(pc, NodeTypeBinOpExpr, token);
     node->data.bin_op_expr.op1 = lhs;
src/parser.hpp
@@ -45,6 +45,7 @@ enum NodeType {
     NodeTypeUse,
     NodeTypeVoid,
     NodeTypeBoolLiteral,
+    NodeTypeNullLiteral,
     NodeTypeIfBoolExpr,
     NodeTypeIfVarExpr,
     NodeTypeWhileExpr,
@@ -161,6 +162,7 @@ enum BinOpType {
     BinOpTypeMult,
     BinOpTypeDiv,
     BinOpTypeMod,
+    BinOpTypeUnwrapMaybe,
 };
 
 struct AstNodeBinOpExpr {
src/tokenizer.cpp
@@ -241,6 +241,8 @@ static void end_token(Tokenize *t) {
         t->cur_tok->id = TokenIdKeywordContinue;
     } else if (mem_eql_str(token_mem, token_len, "break")) {
         t->cur_tok->id = TokenIdKeywordBreak;
+    } else if (mem_eql_str(token_mem, token_len, "null")) {
+        t->cur_tok->id = TokenIdKeywordNull;
     }
 
     t->cur_tok = nullptr;
@@ -418,6 +420,11 @@ void tokenize(Buf *buf, Tokenization *out) {
                 break;
             case TokenizeStateSawQuestionMark:
                 switch (c) {
+                    case '?':
+                        t.cur_tok->id = TokenIdDoubleQuestion;
+                        end_token(&t);
+                        t.state = TokenizeStateStart;
+                        break;
                     case '=':
                         t.cur_tok->id = TokenIdMaybeAssign;
                         end_token(&t);
@@ -1002,6 +1009,7 @@ static const char * token_name(Token *token) {
         case TokenIdKeywordWhile: return "While";
         case TokenIdKeywordContinue: return "Continue";
         case TokenIdKeywordBreak: return "Break";
+        case TokenIdKeywordNull: return "Null";
         case TokenIdLParen: return "LParen";
         case TokenIdRParen: return "RParen";
         case TokenIdComma: return "Comma";
@@ -1052,6 +1060,7 @@ static const char * token_name(Token *token) {
         case TokenIdDot: return "Dot";
         case TokenIdEllipsis: return "Ellipsis";
         case TokenIdMaybe: return "Maybe";
+        case TokenIdDoubleQuestion: return "DoubleQuestion";
         case TokenIdMaybeAssign: return "MaybeAssign";
     }
     return "(invalid token)";
src/tokenizer.hpp
@@ -35,6 +35,7 @@ enum TokenId {
     TokenIdKeywordWhile,
     TokenIdKeywordContinue,
     TokenIdKeywordBreak,
+    TokenIdKeywordNull,
     TokenIdLParen,
     TokenIdRParen,
     TokenIdComma,
@@ -85,6 +86,7 @@ enum TokenId {
     TokenIdDot,
     TokenIdEllipsis,
     TokenIdMaybe,
+    TokenIdDoubleQuestion,
     TokenIdMaybeAssign,
 };
 
std/std.zig
@@ -1,5 +1,6 @@
 use "syscall.zig";
 
+const stdin_fileno : isize = 0;
 const stdout_fileno : isize = 1;
 const stderr_fileno : isize = 2;
 
@@ -38,6 +39,26 @@ pub fn print_i64(x: i64) -> isize {
     return write(stdout_fileno, buf.ptr, len);
 }
 
+/*
+// TODO error handling
+pub fn readline(buf: []u8) -> ?[]u8 {
+    var index = 0;
+    while (index < buf.len) {
+        // TODO unknown size array indexing operator
+        const err = read(stdin_fileno, &buf.ptr[index], 1);
+        if (err != 0) {
+            return null;
+        }
+        // TODO unknown size array indexing operator
+        if (buf.ptr[index] == '\n') {
+            return buf[0...index + 1];
+        }
+        index += 1;
+    }
+    return null;
+}
+*/
+
 fn digit_to_char(digit: u64) -> u8 {
     '0' + (digit as u8)
 }
std/syscall.zig
@@ -1,3 +1,4 @@
+const SYS_read : usize = 0;
 const SYS_write : usize = 1;
 const SYS_exit : usize = 60;
 const SYS_getrandom : usize = 318;
@@ -16,6 +17,10 @@ fn syscall3(number: usize, arg1: usize, arg2: usize, arg3: usize) -> usize {
         : "rcx", "r11")
 }
 
+pub fn read(fd: isize, buf: &u8, count: usize) -> isize {
+    syscall3(SYS_read, fd as usize, buf as usize, count) as isize
+}
+
 pub fn write(fd: isize, buf: &const u8, count: usize) -> isize {
     syscall3(SYS_write, fd as usize, buf as usize, count) as isize
 }
test/run_tests.cpp
@@ -710,7 +710,7 @@ pub fn main(argc : isize, argv : &&u8, env : &&u8) -> i32 {
 
     add_simple_case("maybe type", R"SOURCE(
 use "std.zig";
-pub fn main(argc : isize, argv : &&u8, env : &&u8) -> i32 {
+pub fn main(argc: isize, argv: &&u8, env: &&u8) -> i32 {
     const x : ?bool = true;
 
     if (const y ?= x) {
@@ -722,6 +722,23 @@ pub fn main(argc : isize, argv : &&u8, env : &&u8) -> i32 {
     } else {
         print_str("x is none\n");
     }
+
+    const next_x : ?i32 = null;
+
+    const z = next_x ?? 1234;
+
+    if (z != 1234) {
+        print_str("BAD\n");
+    }
+
+    const final_x : ?i32 = 13;
+
+    const num = final_x ?? unreachable;
+
+    if (num != 13) {
+        print_str("BAD\n");
+    }
+
     return 0;
 }
     )SOURCE", "x is true\n");
CMakeLists.txt
@@ -120,6 +120,7 @@ set(ZIG_STD_SRC
     "${CMAKE_SOURCE_DIR}/std/builtin.zig"
     "${CMAKE_SOURCE_DIR}/std/std.zig"
     "${CMAKE_SOURCE_DIR}/std/syscall.zig"
+    "${CMAKE_SOURCE_DIR}/std/errno.zig"
     "${CMAKE_SOURCE_DIR}/std/rand.zig"
 )