Browse Source

Basic allowance for := and ::

Ginger Bill 8 years ago
parent
commit
9ca2246bac
5 changed files with 477 additions and 26 deletions
  1. 28 24
      code/demo.odin
  2. 147 0
      src/check_stmt.cpp
  3. 150 0
      src/checker.cpp
  4. 77 0
      src/ir.cpp
  5. 75 2
      src/parser.cpp

+ 28 - 24
code/demo.odin

@@ -2,12 +2,12 @@ import "fmt.odin";
 
 proc general_stuff() {
 	// Complex numbers
-	var a = 3 + 4i;
-	var b: complex64 = 3 + 4i;
-	var c: complex128 = 3 + 4i;
-	var d = complex(2, 3);
+	a := 3 + 4i;
+	b: complex64 = 3 + 4i;
+	c: complex128 = 3 + 4i;
+	d := complex(2, 3);
 
-	var e = a / conj(a);
+	e := a / conj(a);
 	fmt.println("(3+4i)/(3-4i) =", e);
 	fmt.println(real(e), "+", imag(e), "i");
 
@@ -15,7 +15,8 @@ proc general_stuff() {
 	// C-style variadic procedures
 	foreign __llvm_core {
 		// The variadic part allows for extra type checking too which C does not provide
-		proc c_printf(fmt: ^u8, #c_vararg args: ..any) -> i32 #link_name "printf";
+		proc c_printf(fmt: ^u8, #c_vararg args: ..any) -> i32
+			#link_name "printf";
 	}
 
 
@@ -27,6 +28,9 @@ proc general_stuff() {
 	var foo = Foo{123, 0.513, "A string"};
 	var x, y, z = expand_to_tuple(foo);
 	fmt.println(x, y, z);
+	compile_assert(type_of(x) == int);
+	compile_assert(type_of(y) == f32);
+	compile_assert(type_of(z) == string);
 
 
 	// By default, all variables are zeroed
@@ -51,7 +55,6 @@ proc foreign_blocks() {
 	// See sys/windows.odin
 }
 
-
 proc default_arguments() {
 	proc hello(a: int = 9, b: int = 9) {
 		fmt.printf("a is %d; b is %d\n", a, b);
@@ -73,9 +76,9 @@ proc named_arguments() {
 	};
 	using Colour;
 
-	proc make_character(name, catch_phrase: string, favorite_color, least_favorite_color: Colour) {
+	proc make_character(name, catch_phrase: string, favourite_colour, least_favourite_colour: Colour) {
 	    fmt.println();
-	    fmt.printf("My name is %v and I like %v.  %v\n", name, favorite_color, catch_phrase);
+	    fmt.printf("My name is %v and I like %v.  %v\n", name, favourite_colour, catch_phrase);
 	}
 
 	make_character("Frank", "¡Ay, caramba!", Blue, Green);
@@ -88,24 +91,24 @@ proc named_arguments() {
 
 	// Named arguments help to disambiguate this problem
 	make_character(catch_phrase = "¡Ay, caramba!", name = "Frank",
-	               least_favorite_color = Green, favorite_color = Blue);
+	               least_favourite_colour = Green, favourite_colour = Blue);
 
 
 	// The named arguments can be specifed in any order.
-	make_character(favorite_color = Octarine, catch_phrase = "U wot m8!",
-	               least_favorite_color = Green, name = "Dennis");
+	make_character(favourite_colour = Octarine, catch_phrase = "U wot m8!",
+	               least_favourite_colour = Green, name = "Dennis");
 
 
 	// NOTE: You cannot mix named arguments with normal values
 	/*
 	make_character("Dennis",
-	               favorite_color = Octarine, catch_phrase = "U wot m8!",
-	               least_favorite_color = Green);
+	               favourite_colour = Octarine, catch_phrase = "U wot m8!",
+	               least_favourite_colour = Green);
 	*/
 
 
 	// Named arguments can also aid with default arguments
-	proc numerous_things(s : string, a = 1, b = 2, c = 3.14,
+	proc numerous_things(s: string, a = 1, b = 2, c = 3.14,
 	                     d = "The Best String!", e = false, f = 10.3/3.1, g = false) {
 		var g_str = g ? "true" : "false";
 		fmt.printf("How many?! %s: %v\n", s, g_str);
@@ -340,21 +343,22 @@ proc explicit_parametric_polymorphic_procedures() {
 	// An alternative to `union`s
 	for entity in entities {
 		match e in entity.derived {
-		case Rock:    fmt.println("Rock",    e.portable_id);
-		case Door:    fmt.println("Door",    e.portable_id);
-		case Monster: fmt.println("Monster", e.portable_id);
+		case Rock:    fmt.println("Rock",    e.portable_id, e.x, e.y);
+		case Door:    fmt.println("Door",    e.portable_id, e.x, e.y);
+		case Monster: fmt.println("Monster", e.portable_id, e.x, e.y);
 		}
 	}
 }
 
+
 proc main() {
 	general_stuff();
-	foreign_blocks();
-	default_arguments();
-	named_arguments();
-	default_return_values();
-	call_location();
-	explicit_parametric_polymorphic_procedures();
+	// foreign_blocks();
+	// default_arguments();
+	// named_arguments();
+	// default_return_values();
+	// call_location();
+	// explicit_parametric_polymorphic_procedures();
 
 	// Command line argument(s)!
 	// -opt=0,1,2,3

+ 147 - 0
src/check_stmt.cpp

@@ -1666,6 +1666,153 @@ void check_stmt_internal(Checker *c, AstNode *node, u32 flags) {
 		c->context = prev_context;
 	case_end;
 
+	case_ast_node(vd, ValueDecl, node);
+		if (vd->is_mutable) {
+			Entity **entities = gb_alloc_array(c->allocator, Entity *, vd->names.count);
+			isize entity_count = 0;
+
+			if (vd->flags & VarDeclFlag_thread_local) {
+				vd->flags &= ~VarDeclFlag_thread_local;
+				error(node, "`thread_local` may only be applied to a variable declaration");
+			}
+
+			for_array(i, vd->names) {
+				AstNode *name = vd->names[i];
+				Entity *entity = NULL;
+				if (name->kind != AstNode_Ident) {
+					error(name, "A variable declaration must be an identifier");
+				} else {
+					Token token = name->Ident;
+					String str = token.string;
+					Entity *found = NULL;
+					// NOTE(bill): Ignore assignments to `_`
+					if (str != "_") {
+						found = current_scope_lookup_entity(c->context.scope, str);
+					}
+					if (found == NULL) {
+						entity = make_entity_variable(c->allocator, c->context.scope, token, NULL, false);
+						entity->identifier = name;
+
+						AstNode *fl = c->context.curr_foreign_library;
+						if (fl != NULL) {
+							GB_ASSERT(fl->kind == AstNode_Ident);
+							entity->Variable.is_foreign = true;
+							entity->Variable.foreign_library_ident = fl;
+						}
+					} else {
+						TokenPos pos = found->token.pos;
+						error(token,
+						      "Redeclaration of `%.*s` in this scope\n"
+						      "\tat %.*s(%td:%td)",
+						      LIT(str), LIT(pos.file), pos.line, pos.column);
+						entity = found;
+					}
+				}
+				if (entity == NULL) {
+					entity = make_entity_dummy_variable(c->allocator, c->global_scope, ast_node_token(name));
+				}
+				entity->parent_proc_decl = c->context.curr_proc_decl;
+				entities[entity_count++] = entity;
+			}
+
+			Type *init_type = NULL;
+			if (vd->type) {
+				init_type = check_type(c, vd->type, NULL);
+				if (init_type == NULL) {
+					init_type = t_invalid;
+				}
+			}
+
+			for (isize i = 0; i < entity_count; i++) {
+				Entity *e = entities[i];
+				GB_ASSERT(e != NULL);
+				if (e->flags & EntityFlag_Visited) {
+					e->type = t_invalid;
+					continue;
+				}
+				e->flags |= EntityFlag_Visited;
+
+				if (e->type == NULL) {
+					e->type = init_type;
+				}
+			}
+
+			check_arity_match(c, vd);
+			check_init_variables(c, entities, entity_count, vd->values, str_lit("variable declaration"));
+
+			for (isize i = 0; i < entity_count; i++) {
+				Entity *e = entities[i];
+				if (e->Variable.is_foreign) {
+					if (vd->values.count > 0) {
+						error(e->token, "A foreign variable declaration cannot have a default value");
+					}
+					init_entity_foreign_library(c, e);
+
+					String name = e->token.string;
+					auto *fp = &c->info.foreigns;
+					HashKey key = hash_string(name);
+					Entity **found = map_get(fp, key);
+					if (found) {
+						Entity *f = *found;
+						TokenPos pos = f->token.pos;
+						Type *this_type = base_type(e->type);
+						Type *other_type = base_type(f->type);
+						if (!are_types_identical(this_type, other_type)) {
+							error(e->token,
+							      "Foreign entity `%.*s` previously declared elsewhere with a different type\n"
+							      "\tat %.*s(%td:%td)",
+							      LIT(name), LIT(pos.file), pos.line, pos.column);
+						}
+					} else {
+						map_set(fp, key, e);
+					}
+				}
+				add_entity(c, c->context.scope, e->identifier, e);
+			}
+
+			if ((vd->flags & VarDeclFlag_using) != 0) {
+				Token token = ast_node_token(node);
+				if (vd->type != NULL && entity_count > 1) {
+					error(token, "`using` can only be applied to one variable of the same type");
+					// TODO(bill): Should a `continue` happen here?
+				}
+
+				for (isize entity_index = 0; entity_index < entity_count; entity_index++) {
+					Entity *e = entities[entity_index];
+					if (e == NULL) {
+						continue;
+					}
+					if (e->kind != Entity_Variable) {
+						continue;
+					}
+					bool is_immutable = e->Variable.is_immutable;
+					String name = e->token.string;
+					Type *t = base_type(type_deref(e->type));
+
+					if (is_type_struct(t) || is_type_raw_union(t)) {
+						Scope *scope = scope_of_node(&c->info, t->Record.node);
+						for_array(i, scope->elements.entries) {
+							Entity *f = scope->elements.entries[i].value;
+							if (f->kind == Entity_Variable) {
+								Entity *uvar = make_entity_using_variable(c->allocator, e, f->token, f->type);
+								uvar->Variable.is_immutable = is_immutable;
+								Entity *prev = scope_insert_entity(c->context.scope, uvar);
+								if (prev != NULL) {
+									error(token, "Namespace collision while `using` `%.*s` of: %.*s", LIT(name), LIT(prev->token.string));
+									return;
+								}
+							}
+						}
+					} else {
+						// NOTE(bill): skip the rest to remove extra errors
+						error(token, "`using` can only be applied to variables of type struct or raw_union");
+						return;
+					}
+				}
+			}
+		}
+	case_end;
+
 
 	case_ast_node(gd, GenDecl, node);
 		GB_ASSERT(!c->context.scope->is_file);

+ 150 - 0
src/checker.cpp

@@ -1387,6 +1387,7 @@ void init_preload(Checker *c) {
 
 
 bool check_arity_match(Checker *c, AstNodeValueSpec *s);
+bool check_arity_match(Checker *c, AstNodeValueDecl *vd);
 void check_collect_entities(Checker *c, Array<AstNode *> nodes, bool is_file_scope);
 void check_collect_entities_from_when_stmt(Checker *c, AstNodeWhenStmt *ws, bool is_file_scope);
 
@@ -1537,6 +1538,37 @@ bool check_arity_match(Checker *c, AstNodeValueSpec *spec) {
 	return true;
 }
 
+
+bool check_arity_match(Checker *c, AstNodeValueDecl *vd) {
+	isize lhs = vd->names.count;
+	isize rhs = vd->values.count;
+
+	if (rhs == 0) {
+		if (vd->type == NULL) {
+			error(vd->names[0], "Missing type or initial expression");
+			return false;
+		}
+	} else if (lhs < rhs) {
+		if (lhs < vd->values.count) {
+			AstNode *n = vd->values[lhs];
+			gbString str = expr_to_string(n);
+			error(n, "Extra initial expression `%s`", str);
+			gb_string_free(str);
+		} else {
+			error(vd->names[0], "Extra initial expression");
+		}
+		return false;
+	} else if (lhs > rhs && rhs != 1) {
+		AstNode *n = vd->names[rhs];
+		gbString str = expr_to_string(n);
+		error(n, "Missing expression for `%s`", str);
+		gb_string_free(str);
+		return false;
+	}
+
+	return true;
+}
+
 void check_collect_entities_from_when_stmt(Checker *c, AstNodeWhenStmt *ws, bool is_file_scope) {
 	Operand operand = {Addressing_Invalid};
 	check_expr(c, &operand, ws->cond);
@@ -1595,6 +1627,124 @@ void check_collect_entities(Checker *c, Array<AstNode *> nodes, bool is_file_sco
 			}
 		case_end;
 
+		case_ast_node(vd, ValueDecl, decl);
+			if (vd->is_mutable) {
+				if (!c->context.scope->is_file) {
+					// NOTE(bill): local scope -> handle later and in order
+					break;
+				}
+
+				// NOTE(bill): You need to store the entity information here unline a constant declaration
+				isize entity_cap = vd->names.count;
+				isize entity_count = 0;
+				Entity **entities = gb_alloc_array(c->allocator, Entity *, entity_cap);
+				DeclInfo *di = NULL;
+				if (vd->values.count > 0) {
+					di = make_declaration_info(heap_allocator(), c->context.scope, c->context.decl);
+					di->entities = entities;
+					di->type_expr = vd->type;
+					di->init_expr = vd->values[0];
+
+
+					if (vd->flags & VarDeclFlag_thread_local) {
+						error(decl, "#thread_local variable declarations cannot have initialization values");
+					}
+				}
+
+
+				for_array(i, vd->names) {
+					AstNode *name = vd->names[i];
+					AstNode *value = NULL;
+					if (i < vd->values.count) {
+						value = vd->values[i];
+					}
+					if (name->kind != AstNode_Ident) {
+						error(name, "A declaration's name must be an identifier, got %.*s", LIT(ast_node_strings[name->kind]));
+						continue;
+					}
+					Entity *e = make_entity_variable(c->allocator, c->context.scope, name->Ident, NULL, false);
+					e->Variable.is_thread_local = (vd->flags & VarDeclFlag_thread_local) != 0;
+					e->identifier = name;
+
+					if (vd->flags & VarDeclFlag_using) {
+						vd->flags &= ~VarDeclFlag_using; // NOTE(bill): This error will be only caught once
+						error(name, "`using` is not allowed at the file scope");
+					}
+
+					AstNode *fl = c->context.curr_foreign_library;
+					if (fl != NULL) {
+						GB_ASSERT(fl->kind == AstNode_Ident);
+						e->Variable.is_foreign = true;
+						e->Variable.foreign_library_ident = fl;
+					}
+
+					entities[entity_count++] = e;
+
+					DeclInfo *d = di;
+					if (d == NULL) {
+						AstNode *init_expr = value;
+						d = make_declaration_info(heap_allocator(), e->scope, c->context.decl);
+						d->type_expr = vd->type;
+						d->init_expr = init_expr;
+					}
+
+					add_entity_and_decl_info(c, name, e, d);
+				}
+
+				if (di != NULL) {
+					di->entity_count = entity_count;
+				}
+
+				check_arity_match(c, vd);
+			} else {
+				for_array(i, vd->names) {
+					AstNode *name = vd->names[i];
+					if (name->kind != AstNode_Ident) {
+						error(name, "A declaration's name must be an identifier, got %.*s", LIT(ast_node_strings[name->kind]));
+						continue;
+					}
+
+					AstNode *init = unparen_expr(vd->values[i]);
+
+					AstNode *fl = c->context.curr_foreign_library;
+					DeclInfo *d = make_declaration_info(c->allocator, c->context.scope, c->context.decl);
+					Entity *e = NULL;
+
+					if (is_ast_node_type(init)) {
+						e = make_entity_type_name(c->allocator, d->scope, name->Ident, NULL);
+						d->type_expr = init;
+						d->init_expr = init;
+					} else if (init->kind == AstNode_ProcLit) {
+						ast_node(pl, ProcLit, init);
+						e = make_entity_procedure(c->allocator, d->scope, name->Ident, NULL, pl->tags);
+						if (fl != NULL) {
+							GB_ASSERT(fl->kind == AstNode_Ident);
+							e->Procedure.foreign_library_ident = fl;
+							pl->tags |= ProcTag_foreign;
+						}
+						GB_PANIC("TODO(bill): Constant procedure literals");
+						d->proc_decl = init;
+						d->type_expr = pl->type;
+					} else {
+						e = make_entity_constant(c->allocator, d->scope, name->Ident, NULL, empty_exact_value);
+						d->type_expr = vd->type;
+						d->init_expr = init;
+					}
+					e->identifier = name;
+
+					if (fl != NULL && e->kind != Entity_Procedure) {
+						error(name, "Only procedures and variables are allowed to be in a foreign block");
+						// continue;
+					}
+
+
+					add_entity_and_decl_info(c, name, e, d);
+				}
+
+				check_arity_match(c, vd);
+			}
+		case_end;
+
 		case_ast_node(gd, GenDecl, decl);
 			AstNodeValueSpec empty_spec = {};
 			AstNodeValueSpec *last_spec = NULL;

+ 77 - 0
src/ir.cpp

@@ -5949,6 +5949,83 @@ void ir_build_stmt_internal(irProcedure *proc, AstNode *node) {
 		ir_build_assign_op(proc, addr, v_one, op);
 	case_end;
 
+	case_ast_node(vd, ValueDecl, node);
+		if (vd->is_mutable) {
+			irModule *m = proc->module;
+			gbTempArenaMemory tmp = gb_temp_arena_memory_begin(&m->tmp_arena);
+
+			if (vd->values.count == 0) { // declared and zero-initialized
+				for_array(i, vd->names) {
+					AstNode *name = vd->names[i];
+					if (!ir_is_blank_ident(name)) {
+						ir_add_local_for_identifier(proc, name, true);
+					}
+				}
+			} else { // Tuple(s)
+				Array<irAddr> lvals = {};
+				Array<irValue *>  inits = {};
+				array_init(&lvals, m->tmp_allocator, vd->names.count);
+				array_init(&inits, m->tmp_allocator, vd->names.count);
+
+				for_array(i, vd->names) {
+					AstNode *name = vd->names[i];
+					irAddr lval = ir_addr(NULL);
+					if (!ir_is_blank_ident(name)) {
+						ir_add_local_for_identifier(proc, name, false);
+						lval = ir_build_addr(proc, name);
+					}
+
+					array_add(&lvals, lval);
+				}
+
+				for_array(i, vd->values) {
+					irValue *init = ir_build_expr(proc, vd->values[i]);
+					Type *t = ir_type(init);
+					if (t->kind == Type_Tuple) {
+						for (isize i = 0; i < t->Tuple.variable_count; i++) {
+							Entity *e = t->Tuple.variables[i];
+							irValue *v = ir_emit_struct_ev(proc, init, i);
+							array_add(&inits, v);
+						}
+					} else {
+						array_add(&inits, init);
+					}
+				}
+
+
+				for_array(i, inits) {
+					ir_addr_store(proc, lvals[i], inits[i]);
+				}
+			}
+
+			gb_temp_arena_memory_end(tmp);
+		} else {
+			for_array(i, vd->names) {
+				AstNode *ident = vd->names[i];
+				GB_ASSERT(ident->kind == AstNode_Ident);
+				Entity *e = entity_of_ident(proc->module->info, ident);
+				GB_ASSERT(e != NULL);
+				if (e->kind == Entity_TypeName) {
+					// NOTE(bill): Generate a new name
+					// parent_proc.name-guid
+					String ts_name = e->token.string;
+					isize name_len = proc->name.len + 1 + ts_name.len + 1 + 10 + 1;
+					u8 *name_text = gb_alloc_array(proc->module->allocator, u8, name_len);
+					i32 guid = cast(i32)proc->module->members.entries.count;
+					name_len = gb_snprintf(cast(char *)name_text, name_len, "%.*s.%.*s-%d", LIT(proc->name), LIT(ts_name), guid);
+					String name = make_string(name_text, name_len-1);
+
+					irValue *value = ir_value_type_name(proc->module->allocator,
+					                                           name, e->type);
+					map_set(&proc->module->entity_names, hash_entity(e), name);
+					ir_gen_global_type_name(proc->module, e, name);
+				} else if (e->kind == Entity_Procedure) {
+					GB_PANIC("TODO(bill): Procedure values");
+				}
+			}
+		}
+	case_end;
+
 	case_ast_node(gd, GenDecl, node);
 		for_array(i, gd->specs) {
 			AstNode *spec = gd->specs[i];

+ 75 - 2
src/parser.cpp

@@ -337,6 +337,15 @@ AST_NODE_KIND(_DeclBegin,      "", i32) \
 		u64              flags; \
 		CommentGroup docs; \
 	}) \
+	AST_NODE_KIND(ValueDecl, "value declaration", struct { \
+		Array<AstNode *> names;      \
+		AstNode *        type;       \
+		Array<AstNode *> values;     \
+		u64              flags;      \
+		bool             is_mutable; \
+		CommentGroup     docs;       \
+		CommentGroup     comment;    \
+	}) \
 	AST_NODE_KIND(ValueSpec, "value specification", struct { \
 		Array<AstNode *> names;  \
 		AstNode *        type;   \
@@ -577,6 +586,7 @@ Token ast_node_token(AstNode *node) {
 	case AstNode_Label:              return node->Label.token;
 
 	case AstNode_GenDecl:            return node->GenDecl.token;
+	case AstNode_ValueDecl:          return ast_node_token(node->ValueDecl.names[0]);
 	case AstNode_ValueSpec:          return ast_node_token(node->ValueSpec.names[0]);
 	case AstNode_ImportSpec:         return node->ImportSpec.import_name;
 	case AstNode_TypeSpec:           return ast_node_token(node->TypeSpec.name);
@@ -817,6 +827,11 @@ AstNode *clone_ast_node(gbAllocator a, AstNode *node) {
 	case AstNode_GenDecl:
 		n->GenDecl.specs = clone_ast_node_array(a, n->GenDecl.specs);
 		break;
+	case AstNode_ValueDecl:
+		n->ValueDecl.names  = clone_ast_node_array(a, n->ValueDecl.names);
+		n->ValueDecl.type   = clone_ast_node(a, n->ValueDecl.type);
+		n->ValueDecl.values = clone_ast_node_array(a, n->ValueDecl.values);
+		break;
 	case AstNode_ValueSpec:
 		n->ValueSpec.names  = clone_ast_node_array(a, n->ValueSpec.names);
 		n->ValueSpec.type   = clone_ast_node(a, n->ValueSpec.type);
@@ -1531,6 +1546,18 @@ AstNode *ast_gen_decl(AstFile *f, Token token, Token open, Token close, Array<As
 	return result;
 }
 
+AstNode *ast_value_decl(AstFile *f, Array<AstNode *> names, AstNode *type, Array<AstNode *> values, bool is_mutable,
+                        CommentGroup docs, CommentGroup comment) {
+	AstNode *result = make_ast_node(f, AstNode_ValueDecl);
+	result->ValueDecl.names      = names;
+	result->ValueDecl.type       = type;
+	result->ValueDecl.values     = values;
+	result->ValueDecl.is_mutable = is_mutable;
+	result->ValueDecl.docs       = docs;
+	result->ValueDecl.comment    = comment;
+	return result;
+}
+
 AstNode *ast_value_spec(AstFile *f, Array<AstNode *> names, AstNode *type, Array<AstNode *> values,
                         CommentGroup docs, CommentGroup comment) {
 	AstNode *result = make_ast_node(f, AstNode_ValueSpec);
@@ -3081,7 +3108,52 @@ AstNode *parse_decl(AstFile *f) {
 	return parse_gen_decl(f, token, func);
 }
 
+AstNode *parse_value_decl(AstFile *f, Array<AstNode *> names, CommentGroup docs) {
+	bool is_mutable = true;
 
+	AstNode *type = NULL;
+	Array<AstNode *> values = {};
+
+	Token colon = expect_token_after(f, Token_Colon, "identifier list");
+	type = parse_type_attempt(f);
+
+	if (f->curr_token.kind == Token_Eq ||
+	    f->curr_token.kind == Token_Colon) {
+		Token sep = f->curr_token; next_token(f);
+		is_mutable = sep.kind != Token_Colon;
+		values = parse_rhs_expr_list(f);
+		if (values.count > names.count) {
+			syntax_error(f->curr_token, "Too many values on the right hand side of the declaration");
+		} else if (values.count < names.count && !is_mutable) {
+			syntax_error(f->curr_token, "All constant declarations must be defined");
+		} else if (values.count == 0) {
+			syntax_error(f->curr_token, "Expected an expression for this declaration");
+		}
+	}
+
+
+	if (is_mutable) {
+		if (type == NULL && values.count == 0) {
+			syntax_error(f->curr_token, "Missing variable type or initialization");
+			return ast_bad_decl(f, f->curr_token, f->curr_token);
+		}
+	} else {
+		if (type == NULL && values.count == 0 && names.count > 0) {
+			syntax_error(f->curr_token, "Missing constant value");
+			return ast_bad_decl(f, f->curr_token, f->curr_token);
+		}
+	}
+
+	if (values.data == NULL) {
+		values = make_ast_node_array(f);
+	}
+
+	if (f->expr_level >= 0) {
+		expect_semicolon(f, NULL);
+	}
+
+	return ast_value_decl(f, names, type, values, is_mutable, docs, f->line_comment);
+}
 
 AstNode *parse_simple_stmt(AstFile *f, StmtAllowFlag flags) {
 	Token token = f->curr_token;
@@ -3091,6 +3163,8 @@ AstNode *parse_simple_stmt(AstFile *f, StmtAllowFlag flags) {
 		return parse_decl(f);
 	}
 
+	CommentGroup docs = f->lead_comment;
+
 	Array<AstNode *> lhs = parse_lhs_expr_list(f);
 	token = f->curr_token;
 	switch (token.kind) {
@@ -3163,8 +3237,7 @@ AstNode *parse_simple_stmt(AstFile *f, StmtAllowFlag flags) {
 			} break;
 			}
 		}
-
-		// return parse_value_decl(f, lhs);
+		return parse_value_decl(f, lhs, docs);
 	}
 
 	if (lhs.count > 1) {