diff --git a/frontends/ast/ast.cc b/frontends/ast/ast.cc
index fe075b270..04360de72 100644
--- a/frontends/ast/ast.cc
+++ b/frontends/ast/ast.cc
@@ -224,6 +224,7 @@ AstNode::AstNode(AstNodeType type, AstNode *child1, AstNode *child2, AstNode *ch
 	port_id = 0;
 	range_left = -1;
 	range_right = 0;
+	unpacked_dimensions = 0;
 	integer = 0;
 	realvalue = 0;
 	id2ast = NULL;
@@ -349,17 +350,15 @@ void AstNode::dumpAst(FILE *f, std::string indent) const
 		fprintf(f, " int=%u", (int)integer);
 	if (realvalue != 0)
 		fprintf(f, " real=%e", realvalue);
-	if (!multirange_dimensions.empty()) {
-		fprintf(f, " multirange=[");
-		for (int v : multirange_dimensions)
-			fprintf(f, " %d", v);
-		fprintf(f, " ]");
-	}
-	if (!multirange_swapped.empty()) {
-		fprintf(f, " multirange_swapped=[");
-		for (bool v : multirange_swapped)
-			fprintf(f, " %d", v);
-		fprintf(f, " ]");
+	if (!dimensions.empty()) {
+		fprintf(f, " dimensions=");
+		for (auto &dim : dimensions) {
+			int left = dim.range_right + dim.range_width - 1;
+			int right = dim.range_right;
+			if (dim.range_swapped)
+				std::swap(left, right);
+			fprintf(f, "[%d:%d]", left, right);
+		}
 	}
 	if (is_enum) {
 		fprintf(f, " type=enum");
@@ -505,6 +504,11 @@ void AstNode::dumpVlog(FILE *f, std::string indent) const
 		}
 		break;
 
+	case AST_MULTIRANGE:
+		for (auto child : children)
+			child->dumpVlog(f, "");
+		break;
+
 	case AST_ALWAYS:
 		fprintf(f, "%s" "always @", indent.c_str());
 		for (auto child : children) {
@@ -542,7 +546,7 @@ void AstNode::dumpVlog(FILE *f, std::string indent) const
 
 	case AST_IDENTIFIER:
 		{
-			AST::AstNode *member_node = AST::get_struct_member(this);
+			AstNode *member_node = get_struct_member();
 			if (member_node)
 				fprintf(f, "%s[%d:%d]", id2vl(str).c_str(), member_node->range_left, member_node->range_right);
 			else
diff --git a/frontends/ast/ast.h b/frontends/ast/ast.h
index c44746131..f05b568be 100644
--- a/frontends/ast/ast.h
+++ b/frontends/ast/ast.h
@@ -202,9 +202,17 @@ namespace AST
 		// set for IDs typed to an enumeration, not used
 		bool is_enum;
 
-		// if this is a multirange memory then this vector contains offset and length of each dimension
-		std::vector<int> multirange_dimensions;
-		std::vector<bool> multirange_swapped; // true if range is swapped
+		// Declared range for array dimension.
+		struct dimension_t {
+			int range_right;     // lsb in [msb:lsb]
+			int range_width;     // msb - lsb + 1
+			bool range_swapped;  // if the declared msb < lsb, msb and lsb above are swapped
+		};
+		// Packed and unpacked dimensions for arrays.
+		// Unpacked dimensions go first, to follow the order of indexing.
+		std::vector<dimension_t> dimensions;
+		// Number of unpacked dimensions.
+		int unpacked_dimensions;
 
 		// this is set by simplify and used during RTLIL generation
 		AstNode *id2ast;
@@ -371,6 +379,10 @@ namespace AST
 		// localized fixups after modifying children/attributes of a particular node
 		void fixup_hierarchy_flags(bool force_descend = false);
 
+		// helpers for indexing
+		AstNode *make_index_range(AstNode *node, bool unpacked_range = false);
+		AstNode *get_struct_member() const;
+
 		// helper to print errors from simplify/genrtlil code
 		[[noreturn]] void input_error(const char *format, ...) const YS_ATTRIBUTE(format(printf, 2, 3));
 	};
@@ -416,10 +428,6 @@ namespace AST
 	// Helper for setting the src attribute.
 	void set_src_attr(RTLIL::AttrObject *obj, const AstNode *ast);
 
-	// struct helper exposed from simplify for genrtlil
-	AstNode *make_struct_member_range(AstNode *node, AstNode *member_node);
-	AstNode *get_struct_member(const AstNode *node);
-
 	// generate standard $paramod... derived module name; parameters should be
 	// in the order they are declared in the instantiated module
 	std::string derived_module_name(std::string stripped_name, const std::vector<std::pair<RTLIL::IdString, RTLIL::Const>> &parameters);
diff --git a/frontends/ast/genrtlil.cc b/frontends/ast/genrtlil.cc
index 03697ebf3..fe67f00c6 100644
--- a/frontends/ast/genrtlil.cc
+++ b/frontends/ast/genrtlil.cc
@@ -1045,7 +1045,7 @@ void AstNode::detectSignWidthWorker(int &width_hint, bool &sign_hint, bool *foun
 			if (children.size() > 1)
 				range = children[1];
 		} else if (id_ast->type == AST_STRUCT_ITEM || id_ast->type == AST_STRUCT || id_ast->type == AST_UNION) {
-			AstNode *tmp_range = make_struct_member_range(this, id_ast);
+			AstNode *tmp_range = make_index_range(id_ast);
 			this_width = tmp_range->range_left - tmp_range->range_right + 1;
 			delete tmp_range;
 		} else
@@ -1584,7 +1584,7 @@ RTLIL::SigSpec AstNode::genRTLIL(int width_hint, bool sign_hint)
 			chunk.width = wire->width;
 			chunk.offset = 0;
 
-			if ((member_node = get_struct_member(this))) {
+			if ((member_node = get_struct_member())) {
 				// Clamp wire chunk to range of member within struct/union.
 				chunk.width = member_node->range_left - member_node->range_right + 1;
 				chunk.offset = member_node->range_right;
diff --git a/frontends/ast/simplify.cc b/frontends/ast/simplify.cc
index 8e0de2994..1b8bd9dc2 100644
--- a/frontends/ast/simplify.cc
+++ b/frontends/ast/simplify.cc
@@ -259,35 +259,24 @@ static int range_width(AstNode *node, AstNode *rnode)
 {
 	log_assert(rnode->type==AST_RANGE);
 	if (!rnode->range_valid) {
-		node->input_error("Size must be constant in packed struct/union member %s\n", node->str.c_str());
-
+		node->input_error("Non-constant range in declaration of %s\n", node->str.c_str());
 	}
 	// note: range swapping has already been checked for
 	return rnode->range_left - rnode->range_right + 1;
 }
 
+static int add_dimension(AstNode *node, AstNode *rnode)
+{
+	int width = range_width(node, rnode);
+	node->dimensions.push_back({ rnode->range_right, width, rnode->range_swapped });
+	return width;
+}
+
 [[noreturn]] static void struct_array_packing_error(AstNode *node)
 {
 	node->input_error("Unpacked array in packed struct/union member %s\n", node->str.c_str());
 }
 
-static void save_struct_range_dimensions(AstNode *node, AstNode *rnode)
-{
-	node->multirange_dimensions.push_back(rnode->range_right);
-	node->multirange_dimensions.push_back(range_width(node, rnode));
-	node->multirange_swapped.push_back(rnode->range_swapped);
-}
-
-static int get_struct_range_offset(AstNode *node, int dimension)
-{
-	return node->multirange_dimensions[2*dimension];
-}
-
-static int get_struct_range_width(AstNode *node, int dimension)
-{
-	return node->multirange_dimensions[2*dimension + 1];
-}
-
 static int size_packed_struct(AstNode *snode, int base_offset)
 {
 	// Struct members will be laid out in the structure contiguously from left to right.
@@ -303,10 +292,6 @@ static int size_packed_struct(AstNode *snode, int base_offset)
 		if (node->type == AST_STRUCT || node->type == AST_UNION) {
 			// embedded struct or union
 			width = size_packed_struct(node, base_offset + offset);
-			// set range of struct
-			node->range_right = base_offset + offset;
-			node->range_left = base_offset + offset + width - 1;
-			node->range_valid = true;
 		}
 		else {
 			log_assert(node->type == AST_STRUCT_ITEM);
@@ -318,18 +303,16 @@ static int size_packed_struct(AstNode *snode, int base_offset)
 					// and integer data types are allowed in packed structs / unions in SystemVerilog.
 					if (node->children[1]->type == AST_RANGE) {
 						// Unpacked array, e.g. bit [63:0] a [0:3]
+						// Pretend it's declared as a packed array, e.g. bit [0:3][63:0] a
 						auto rnode = node->children[1];
 						if (rnode->children.size() == 1) {
 							// C-style array size, e.g. bit [63:0] a [4]
-							node->multirange_dimensions.push_back(0);
-							node->multirange_dimensions.push_back(rnode->range_left);
-							node->multirange_swapped.push_back(true);
+							node->dimensions.push_back({ 0, rnode->range_left, true });
 							width *= rnode->range_left;
 						} else {
-							save_struct_range_dimensions(node, rnode);
-							width *= range_width(node, rnode);
+							width *= add_dimension(node, rnode);
 						}
-						save_struct_range_dimensions(node, node->children[0]);
+						add_dimension(node, node->children[0]);
 					}
 					else {
 						// The Yosys extension for unpacked arrays in packed structs / unions
@@ -338,7 +321,7 @@ static int size_packed_struct(AstNode *snode, int base_offset)
 					}
 				} else {
 					// Vector
-					save_struct_range_dimensions(node, node->children[0]);
+					add_dimension(node, node->children[0]);
 				}
 				// range nodes are now redundant
 				for (AstNode *child : node->children)
@@ -354,8 +337,7 @@ static int size_packed_struct(AstNode *snode, int base_offset)
 				}
 				width = 1;
 				for (auto rnode : node->children[0]->children) {
-					save_struct_range_dimensions(node, rnode);
-					width *= range_width(node, rnode);
+					width *= add_dimension(node, rnode);
 				}
 				// range nodes are now redundant
 				for (AstNode *child : node->children)
@@ -365,6 +347,7 @@ static int size_packed_struct(AstNode *snode, int base_offset)
 			else if (node->range_left < 0) {
 				// 1 bit signal: bit, logic or reg
 				width = 1;
+				node->dimensions.push_back({ 0, width, false });
 			}
 			else {
 				// already resolved and compacted
@@ -395,12 +378,16 @@ static int size_packed_struct(AstNode *snode, int base_offset)
 			offset += width;
 		}
 	}
-	return (is_union ? packed_width : offset);
-}
 
-[[noreturn]] static void struct_op_error(AstNode *node)
-{
-	node->input_error("Unsupported operation for struct/union member %s\n", node->str.c_str()+1);
+	int width = is_union ? packed_width : offset;
+
+	snode->range_right = base_offset;
+	snode->range_left = base_offset + width - 1;
+	snode->range_valid = true;
+	if (snode->dimensions.empty())
+		snode->dimensions.push_back({ 0, width, false });
+
+	return width;
 }
 
 static AstNode *node_int(int ival)
@@ -413,113 +400,123 @@ static AstNode *multiply_by_const(AstNode *expr_node, int stride)
 	return new AstNode(AST_MUL, expr_node, node_int(stride));
 }
 
-static AstNode *normalize_struct_index(AstNode *expr, AstNode *member_node, int dimension)
+static AstNode *normalize_index(AstNode *expr, AstNode *decl_node, int dimension)
 {
 	expr = expr->clone();
 
-	int offset = get_struct_range_offset(member_node, dimension);
+	int offset = decl_node->dimensions[dimension].range_right;
 	if (offset) {
 		expr = new AstNode(AST_SUB, expr, node_int(offset));
 	}
 
-	if (member_node->multirange_swapped[dimension]) {
-		// The dimension has swapped range; swap index into the struct accordingly.
-		int msb = get_struct_range_width(member_node, dimension) - 1;
-		expr = new AstNode(AST_SUB, node_int(msb), expr);
+	// Packed dimensions are normally indexed by lsb, while unpacked dimensions are normally indexed by msb.
+	if ((dimension < decl_node->unpacked_dimensions) ^ decl_node->dimensions[dimension].range_swapped) {
+		// Swap the index if the dimension is declared the "wrong" way.
+		int left = decl_node->dimensions[dimension].range_width - 1;
+		expr = new AstNode(AST_SUB, node_int(left), expr);
 	}
 
 	return expr;
 }
 
-static AstNode *struct_index_lsb_offset(AstNode *lsb_offset, AstNode *rnode, AstNode *member_node, int dimension, int &stride)
+static AstNode *index_offset(AstNode *offset, AstNode *rnode, AstNode *decl_node, int dimension, int &stride)
 {
-	stride /= get_struct_range_width(member_node, dimension);
-	auto right = normalize_struct_index(rnode->children.back(), member_node, dimension);
-	auto offset = stride > 1 ? multiply_by_const(right, stride) : right;
-	return lsb_offset ? new AstNode(AST_ADD, lsb_offset, offset) : offset;
+	stride /= decl_node->dimensions[dimension].range_width;
+	auto right = normalize_index(rnode->children.back(), decl_node, dimension);
+	auto add_offset = stride > 1 ? multiply_by_const(right, stride) : right;
+	return offset ? new AstNode(AST_ADD, offset, add_offset) : add_offset;
 }
 
-static AstNode *struct_index_msb_offset(AstNode *lsb_offset, AstNode *rnode, AstNode *member_node, int dimension, int stride)
+static AstNode *index_msb_offset(AstNode *lsb_offset, AstNode *rnode, AstNode *decl_node, int dimension, int stride)
 {
 	log_assert(rnode->children.size() <= 2);
 
 	// Offset to add to LSB
-	AstNode *offset;
+	AstNode *add_offset;
 	if (rnode->children.size() == 1) {
 		// Index, e.g. s.a[i]
-		offset = node_int(stride - 1);
+		add_offset = node_int(stride - 1);
 	}
 	else {
 		// rnode->children.size() == 2
 		// Slice, e.g. s.a[i:j]
-		auto left = normalize_struct_index(rnode->children[0], member_node, dimension);
-		auto right = normalize_struct_index(rnode->children[1], member_node, dimension);
-		offset = new AstNode(AST_SUB, left, right);
+		auto left = normalize_index(rnode->children[0], decl_node, dimension);
+		auto right = normalize_index(rnode->children[1], decl_node, dimension);
+		add_offset = new AstNode(AST_SUB, left, right);
 		if (stride > 1) {
 			// offset = (msb - lsb + 1)*stride - 1
-			auto slice_width = new AstNode(AST_ADD, offset, node_int(1));
-			offset = new AstNode(AST_SUB, multiply_by_const(slice_width, stride), node_int(1));
+			auto slice_width = new AstNode(AST_ADD, add_offset, node_int(1));
+			add_offset = new AstNode(AST_SUB, multiply_by_const(slice_width, stride), node_int(1));
 		}
 	}
 
-	return new AstNode(AST_ADD, lsb_offset, offset);
+	return new AstNode(AST_ADD, lsb_offset, add_offset);
 }
 
 
-AstNode *AST::make_struct_member_range(AstNode *node, AstNode *member_node)
+AstNode *AstNode::make_index_range(AstNode *decl_node, bool unpacked_range)
 {
 	// Work out the range in the packed array that corresponds to a struct member
 	// taking into account any range operations applicable to the current node
 	// such as array indexing or slicing
-	int range_left = member_node->range_left;
-	int range_right = member_node->range_right;
-	if (node->children.empty()) {
+	if (children.empty()) {
 		// no range operations apply, return the whole width
-		return make_range(range_left - range_right, 0);
+		return make_range(decl_node->range_left - decl_node->range_right, 0);
 	}
 
-	if (node->children.size() != 1) {
-		struct_op_error(node);
-	}
+	log_assert(children.size() == 1);
 
 	// Range operations
-	auto rnode = node->children[0];
-	AstNode *lsb_offset = NULL;
-	int stride = range_left - range_right + 1;
-	size_t i = 0;
+	AstNode *rnode = children[0];
+	AstNode *offset = NULL;
+	int dim = unpacked_range ? 0 : decl_node->unpacked_dimensions;
+	int max_dim = unpacked_range ? decl_node->unpacked_dimensions : GetSize(decl_node->dimensions);
+
+	int stride = 1;
+	for (int i = dim; i < max_dim; i++) {
+		stride *= decl_node->dimensions[i].range_width;
+	}
 
 	// Calculate LSB offset for the final index / slice
 	if (rnode->type == AST_RANGE) {
-		lsb_offset = struct_index_lsb_offset(lsb_offset, rnode, member_node, i, stride);
+		offset = index_offset(offset, rnode, decl_node, dim, stride);
 	}
 	else if (rnode->type == AST_MULTIRANGE) {
 		// Add offset for each dimension
-		auto mrnode = rnode;
-		for (i = 0; i < mrnode->children.size(); i++) {
-			rnode = mrnode->children[i];
-			lsb_offset = struct_index_lsb_offset(lsb_offset, rnode, member_node, i, stride);
+		AstNode *mrnode = rnode;
+		int stop_dim = std::min(GetSize(mrnode->children), max_dim);
+		for (; dim < stop_dim; dim++) {
+			rnode = mrnode->children[dim];
+			offset = index_offset(offset, rnode, decl_node, dim, stride);
 		}
-		i--;  // Step back to the final index / slice
+		dim--;  // Step back to the final index / slice
 	}
 	else {
-		struct_op_error(node);
+		input_error("Unsupported range operation for %s\n", str.c_str());
 	}
 
-	// Calculate MSB offset for the final index / slice
-	auto msb_offset = struct_index_msb_offset(lsb_offset->clone(), rnode, member_node, i, stride);
+	AstNode *index_range = new AstNode(AST_RANGE);
 
-	return new AstNode(AST_RANGE, msb_offset, lsb_offset);
+	if (!unpacked_range && (stride > 1 || GetSize(rnode->children) == 2)) {
+		// Calculate MSB offset for the final index / slice of packed dimensions.
+		AstNode *msb_offset = index_msb_offset(offset->clone(), rnode, decl_node, dim, stride);
+		index_range->children.push_back(msb_offset);
+	}
+
+	index_range->children.push_back(offset);
+
+	return index_range;
 }
 
-AstNode *AST::get_struct_member(const AstNode *node)
+AstNode *AstNode::get_struct_member() const
 {
-	AST::AstNode *member_node;
-	if (node->attributes.count(ID::wiretype) && (member_node = node->attributes.at(ID::wiretype)) &&
+	AstNode *member_node;
+	if (attributes.count(ID::wiretype) && (member_node = attributes.at(ID::wiretype)) &&
 	    (member_node->type == AST_STRUCT_ITEM || member_node->type == AST_STRUCT || member_node->type == AST_UNION))
 	{
 		return member_node;
 	}
-	return NULL;
+	return nullptr;
 }
 
 static void add_members_to_scope(AstNode *snode, std::string name)
@@ -537,22 +534,10 @@ static void add_members_to_scope(AstNode *snode, std::string name)
 	}
 }
 
-static int get_max_offset(AstNode *node)
-{
-	// get the width from the MS member in the struct
-	// as members are laid out from left to right in the packed wire
-	log_assert(node->type==AST_STRUCT || node->type==AST_UNION);
-	while (node->type != AST_STRUCT_ITEM) {
-		node = node->children[0];
-	}
-	return node->range_left;
-}
-
 static AstNode *make_packed_struct(AstNode *template_node, std::string &name, decltype(AstNode::attributes) &attributes)
 {
 	// create a wire for the packed struct
-	int offset = get_max_offset(template_node);
-	auto wnode = new AstNode(AST_WIRE, make_range(offset, 0));
+	auto wnode = new AstNode(AST_WIRE, make_range(template_node->range_left, 0));
 	wnode->str = name;
 	wnode->is_logic = true;
 	wnode->range_valid = true;
@@ -560,6 +545,8 @@ static AstNode *make_packed_struct(AstNode *template_node, std::string &name, de
 	for (auto &pair : attributes) {
 		wnode->set_attribute(pair.first, pair.second->clone());
 	}
+	// resolve packed dimension
+	while (wnode->simplify(true, 1, -1, false)) {}
 	// make sure this node is the one in scope for this name
 	current_scope[name] = wnode;
 	// add all the struct members to scope under the wire's name
@@ -1986,6 +1973,7 @@ bool AstNode::simplify(bool const_fold, int stage, int width_hint, bool sign_hin
 			range_swapped = template_node->range_swapped;
 			range_left = template_node->range_left;
 			range_right = template_node->range_right;
+
 			set_attribute(ID::wiretype, mkconst_str(resolved_type_node->str));
 			for (auto template_child : template_node->children)
 				children.push_back(template_child->clone());
@@ -2046,9 +2034,7 @@ bool AstNode::simplify(bool const_fold, int stage, int width_hint, bool sign_hin
 		if (old_range_valid != range_valid)
 			did_something = true;
 		if (range_valid && range_right > range_left) {
-			int tmp = range_right;
-			range_right = range_left;
-			range_left = tmp;
+			std::swap(range_left, range_right);
 			range_swapped = true;
 		}
 	}
@@ -2093,58 +2079,83 @@ bool AstNode::simplify(bool const_fold, int stage, int width_hint, bool sign_hin
 		}
 	}
 
-	// resolve multiranges on memory decl
-	if (type == AST_MEMORY && children.size() > 1 && children[1]->type == AST_MULTIRANGE)
-	{
-		int total_size = 1;
-		multirange_dimensions.clear();
-		multirange_swapped.clear();
-		for (auto range : children[1]->children) {
-			if (!range->range_valid)
-				input_error("Non-constant range on memory decl.\n");
-			multirange_dimensions.push_back(min(range->range_left, range->range_right));
-			multirange_dimensions.push_back(max(range->range_left, range->range_right) - min(range->range_left, range->range_right) + 1);
-			multirange_swapped.push_back(range->range_swapped);
-			total_size *= multirange_dimensions.back();
+	// Resolve packed and unpacked ranges in declarations.
+	if ((type == AST_WIRE || type == AST_MEMORY) && dimensions.empty()) {
+		if (!children.empty()) {
+			// Unpacked ranges first, then packed ranges.
+			for (int i = std::min(GetSize(children), 2) - 1; i >= 0; i--) {
+				if (children[i]->type == AST_MULTIRANGE) {
+					int width = 1;
+					for (auto range : children[i]->children) {
+						width *= add_dimension(this, range);
+						if (i) unpacked_dimensions++;
+					}
+					delete children[i];
+					int left = width - 1, right = 0;
+					if (i)
+						std::swap(left, right);
+					children[i] = new AstNode(AST_RANGE, mkconst_int(left, true), mkconst_int(right, true));
+					fixup_hierarchy_flags();
+					did_something = true;
+				} else if (children[i]->type == AST_RANGE) {
+					add_dimension(this, children[i]);
+					if (i) unpacked_dimensions++;
+				}
+			}
+		} else {
+			// 1 bit signal: bit, logic or reg
+			dimensions.push_back({ 0, 1, false });
 		}
-		delete children[1];
-		children[1] = new AstNode(AST_RANGE, AstNode::mkconst_int(0, true), AstNode::mkconst_int(total_size-1, true));
-		fixup_hierarchy_flags();
-		did_something = true;
 	}
 
-	// resolve multiranges on memory access
-	if (type == AST_IDENTIFIER && id2ast && id2ast->type == AST_MEMORY && children.size() > 0 && children[0]->type == AST_MULTIRANGE)
+	// Resolve multidimensional array access.
+	if (type == AST_IDENTIFIER && !basic_prep && id2ast && (id2ast->type == AST_WIRE || id2ast->type == AST_MEMORY) &&
+	    children.size() > 0 && (children[0]->type == AST_RANGE || children[0]->type == AST_MULTIRANGE))
 	{
-		AstNode *index_expr = nullptr;
+		int dims_sel = children[0]->type == AST_MULTIRANGE ? children[0]->children.size() : 1;
+		// Save original number of dimensions for $size() etc.
+		integer = dims_sel;
 
-		integer = children[0]->children.size(); // save original number of dimensions for $size() etc.
-		for (int i = 0; 2*i < GetSize(id2ast->multirange_dimensions); i++)
-		{
-			if (GetSize(children[0]->children) <= i)
-				input_error("Insufficient number of array indices for %s.\n", log_id(str));
+		// Split access into unpacked and packed parts.
+		AstNode *unpacked_range = nullptr;
+		AstNode *packed_range = nullptr;
 
-			AstNode *new_index_expr = children[0]->children[i]->children.at(0)->clone();
-
-			if (id2ast->multirange_dimensions[2*i])
-				new_index_expr = new AstNode(AST_SUB, new_index_expr, AstNode::mkconst_int(id2ast->multirange_dimensions[2*i], true));
-
-			if (i == 0)
-				index_expr = new_index_expr;
-			else
-				index_expr = new AstNode(AST_ADD, new AstNode(AST_MUL, index_expr, AstNode::mkconst_int(id2ast->multirange_dimensions[2*i+1], true)), new_index_expr);
+		if (id2ast->unpacked_dimensions) {
+			if (id2ast->unpacked_dimensions > 1) {
+				// Flattened range for access to unpacked dimensions.
+				unpacked_range = make_index_range(id2ast, true);
+			} else {
+				// Index into one-dimensional unpacked part; unlink simple range node.
+				AstNode *&range = children[0]->type == AST_MULTIRANGE ? children[0]->children[0] : children[0];
+				unpacked_range = range;
+				range = nullptr;
+			}
 		}
 
-		for (int i = GetSize(id2ast->multirange_dimensions)/2; i < GetSize(children[0]->children); i++)
-			children.push_back(children[0]->children[i]->clone());
+		if (dims_sel > id2ast->unpacked_dimensions) {
+			if (GetSize(id2ast->dimensions) - id2ast->unpacked_dimensions > 1) {
+				// Flattened range for access to packed dimensions.
+				packed_range = make_index_range(id2ast, false);
+			} else {
+				// Index into one-dimensional packed part; unlink simple range node.
+				AstNode *&range = children[0]->type == AST_MULTIRANGE ? children[0]->children[dims_sel - 1] : children[0];
+				packed_range = range;
+				range = nullptr;
+			}
+		}
 
-		delete children[0];
-		if (index_expr == nullptr)
-			children.erase(children.begin());
-		else
-			children[0] = new AstNode(AST_RANGE, index_expr);
+		for (auto &it : children)
+			delete it;
+		children.clear();
+
+		if (unpacked_range)
+			children.push_back(unpacked_range);
+
+		if (packed_range)
+			children.push_back(packed_range);
 
 		fixup_hierarchy_flags();
+		basic_prep = true;
 		did_something = true;
 	}
 
@@ -2210,12 +2221,12 @@ bool AstNode::simplify(bool const_fold, int stage, int width_hint, bool sign_hin
 
 				if (found_sname) {
 					// structure member, rewrite this node to reference the packed struct wire
-					auto range = make_struct_member_range(this, item_node);
+					auto range = make_index_range(item_node);
 					newNode = new AstNode(AST_IDENTIFIER, range);
 					newNode->str = sname;
 					// save type and original number of dimensions for $size() etc.
 					newNode->set_attribute(ID::wiretype, item_node->clone());
-					if (!item_node->multirange_dimensions.empty() && children.size() > 0) {
+					if (!item_node->dimensions.empty() && children.size() > 0) {
 						if (children[0]->type == AST_RANGE)
 							newNode->integer = 1;
 						else if (children[0]->type == AST_MULTIRANGE)
@@ -2835,7 +2846,7 @@ bool AstNode::simplify(bool const_fold, int stage, int width_hint, bool sign_hin
 		if (!children[0]->id2ast->range_valid)
 			goto skip_dynamic_range_lvalue_expansion;
 
-		AST::AstNode *member_node = get_struct_member(children[0]);
+		AST::AstNode *member_node = children[0]->get_struct_member();
 		int wire_width = member_node ?
 			member_node->range_left - member_node->range_right + 1 :
 			children[0]->id2ast->range_left - children[0]->id2ast->range_right + 1;
@@ -2881,7 +2892,7 @@ bool AstNode::simplify(bool const_fold, int stage, int width_hint, bool sign_hin
 				int dims = children[0]->integer;
 				stride = wire_width;
 				for (int dim = 0; dim < dims; dim++) {
-					stride /= get_struct_range_width(member_node, dim);
+					stride /= member_node->dimensions[dim].range_width;
 				}
 				bitno_div = stride;
 			} else {
@@ -3042,6 +3053,8 @@ skip_dynamic_range_lvalue_expansion:;
 	// found right-hand side identifier for memory -> replace with memory read port
 	if (stage > 1 && type == AST_IDENTIFIER && id2ast != NULL && id2ast->type == AST_MEMORY && !in_lvalue &&
 			children.size() == 1 && children[0]->type == AST_RANGE && children[0]->children.size() == 1) {
+		if (integer < (unsigned)id2ast->unpacked_dimensions)
+			input_error("Insufficient number of array indices for %s.\n", log_id(str));
 		newNode = new AstNode(AST_MEMRD, children[0]->children[0]->clone());
 		newNode->str = str;
 		newNode->id2ast = id2ast;
@@ -3100,6 +3113,9 @@ skip_dynamic_range_lvalue_expansion:;
 			children[0]->id2ast->children[0]->range_valid && children[0]->id2ast->children[1]->range_valid &&
 			(children[0]->children.size() == 1 || children[0]->children.size() == 2) && children[0]->children[0]->type == AST_RANGE)
 	{
+		if (children[0]->integer < (unsigned)children[0]->id2ast->unpacked_dimensions)
+			input_error("Insufficient number of array indices for %s.\n", log_id(str));
+
 		std::stringstream sstr;
 		sstr << "$memwr$" << children[0]->str << "$" << RTLIL::encode_filename(filename) << ":" << location.first_line << "$" << (autoidx++);
 		std::string id_addr = sstr.str() + "_ADDR", id_data = sstr.str() + "_DATA", id_en = sstr.str() + "_EN";
@@ -3495,8 +3511,6 @@ skip_dynamic_range_lvalue_expansion:;
 				int result, high = 0, low = 0, left = 0, right = 0, width = 1; // defaults for a simple wire
 				AstNode *id_ast = NULL;
 
-				// Is this needed?
-				//while (buf->simplify(true, false, stage, width_hint, sign_hint, false)) { }
 				buf->detectSignWidth(width_hint, sign_hint);
 
 				if (buf->type == AST_IDENTIFIER) {
@@ -3506,99 +3520,27 @@ skip_dynamic_range_lvalue_expansion:;
 					if (!id_ast)
 						input_error("Failed to resolve identifier %s for width detection!\n", buf->str.c_str());
 
-					// Check for item in packed struct / union
-					AST::AstNode *item_node = get_struct_member(buf);
-					if (id_ast->type == AST_WIRE && item_node) {
+					if (id_ast->type == AST_WIRE || id_ast->type == AST_MEMORY) {
+						// Check for item in packed struct / union
+						AstNode *item_node = buf->get_struct_member();
+						if (item_node)
+							id_ast = item_node;
+
 						// The dimension of the original array expression is saved in the 'integer' field
 						dim += buf->integer;
-						if (item_node->multirange_dimensions.empty()) {
-							if (dim != 1)
-								input_error("Dimension %d out of range in `%s', as it only has one dimension!\n", dim, item_node->str.c_str());
-							left  = high = item_node->range_left;
-							right = low  = item_node->range_right;
-						} else {
-							int dims = GetSize(item_node->multirange_dimensions)/2;
-							if (dim < 1 || dim > dims)
-								input_error("Dimension %d out of range in `%s', as it only has dimensions 1..%d!\n", dim, item_node->str.c_str(), dims);
-							right = low  = get_struct_range_offset(item_node, dim - 1);
-							left  = high = low + get_struct_range_width(item_node, dim - 1) - 1;
-							if (item_node->multirange_swapped[dim - 1]) {
-								std::swap(left, right);
-							}
-							for (int i = dim; i < dims; i++) {
-							    mem_depth *= get_struct_range_width(item_node, i);
-							}
-						}
-					}
-					// Otherwise, we have 4 cases:
-					// wire x;                ==> AST_WIRE, no AST_RANGE children
-					// wire [1:0]x;           ==> AST_WIRE, AST_RANGE children
-					// wire [1:0]x[1:0];      ==> AST_MEMORY, two AST_RANGE children (1st for packed, 2nd for unpacked)
-					// wire [1:0]x[1:0][1:0]; ==> AST_MEMORY, one AST_RANGE child (0) for packed, then AST_MULTIRANGE child (1) for unpacked
-					// (updated: actually by the time we are here, AST_MULTIRANGE is converted into one big AST_RANGE)
-					// case 0 handled by default
-					else if ((id_ast->type == AST_WIRE || id_ast->type == AST_MEMORY) && id_ast->children.size() > 0) {
-						// handle packed array left/right for case 1, and cases 2/3 when requesting the last dimension (packed side)
-						AstNode *wire_range = id_ast->children[0];
-						left = wire_range->children[0]->integer;
-						right = wire_range->children[1]->integer;
-						high = max(left, right);
-						low  = min(left, right);
-					}
-					if (id_ast->type == AST_MEMORY) {
-						// a slice of our identifier means we advance to the next dimension, e.g. $size(a[3])
-						if (buf->children.size() > 0) {
-							// something is hanging below this identifier
-							if (buf->children[0]->type == AST_RANGE && buf->integer == 0)
-								// if integer == 0, this node was originally created as AST_RANGE so it's dimension is 1
-								dim++;
-							// more than one range, e.g. $size(a[3][2])
-							else // created an AST_MULTIRANGE, converted to AST_RANGE, but original dimension saved in 'integer' field
-								dim += buf->integer; // increment by multirange size
-						}
 
-						// We got here only if the argument is a memory
-						// Otherwise $size() and $bits() return the expression width
-						AstNode *mem_range = id_ast->children[1];
-						if (str == "\\$bits") {
-							if (mem_range->type == AST_RANGE) {
-								if (!mem_range->range_valid)
-									input_error("Failed to detect width of memory access `%s'!\n", buf->str.c_str());
-								mem_depth = mem_range->range_left - mem_range->range_right + 1;
-							} else
-								input_error("Unknown memory depth AST type in `%s'!\n", buf->str.c_str());
-						} else {
-							// $size(), $left(), $right(), $high(), $low()
-							int dims = 1;
-							if (mem_range->type == AST_RANGE) {
-								if (id_ast->multirange_dimensions.empty()) {
-									if (!mem_range->range_valid)
-										input_error("Failed to detect width of memory access `%s'!\n", buf->str.c_str());
-									if (dim == 1) {
-										left  = mem_range->range_right;
-										right = mem_range->range_left;
-										high = max(left, right);
-										low  = min(left, right);
-									}
-								} else {
-									dims = GetSize(id_ast->multirange_dimensions)/2;
-									if (dim <= dims) {
-										width_hint = id_ast->multirange_dimensions[2*dim-1];
-										high = id_ast->multirange_dimensions[2*dim-2] + id_ast->multirange_dimensions[2*dim-1] - 1;
-										low  = id_ast->multirange_dimensions[2*dim-2];
-										if (id_ast->multirange_swapped[dim-1]) {
-											left = low;
-											right = high;
-										} else {
-											right = low;
-											left = high;
-										}
-									} else if ((dim > dims+1) || (dim < 0))
-										input_error("Dimension %d out of range in `%s', as it only has dimensions 1..%d!\n", dim, buf->str.c_str(), dims+1);
-								}
-							} else {
-								input_error("Unknown memory depth AST type in `%s'!\n", buf->str.c_str());
-							}
+						int dims = GetSize(id_ast->dimensions);
+						// TODO: IEEE Std 1800-2017 20.7: "If the first argument to an array query function would cause $dimensions to return 0
+						// or if the second argument is out of range, then 'x shall be returned."
+						if (dim < 1 || dim > dims)
+							input_error("Dimension %d out of range in `%s', as it only has %d dimensions!\n", dim, id_ast->str.c_str(), dims);
+						right = low  = id_ast->dimensions[dim - 1].range_right;
+						left  = high = low + id_ast->dimensions[dim - 1].range_width - 1;
+						if (id_ast->dimensions[dim - 1].range_swapped) {
+							std::swap(left, right);
+						}
+						for (int i = dim; i < dims; i++) {
+							mem_depth *= id_ast->dimensions[i].range_width;
 						}
 					}
 					width = high - low + 1;
@@ -4180,7 +4122,7 @@ replace_fcall_later:;
 							tmp_range_left = (param_width + 2*param_offset) - children[0]->range_right - 1;
 							tmp_range_right = (param_width + 2*param_offset) - children[0]->range_left - 1;
 						}
-						AST::AstNode *member_node = get_struct_member(this);
+						AstNode *member_node = get_struct_member();
 						int chunk_offset = member_node ? member_node->range_right : 0;
 						log_assert(!(chunk_offset && param_upto));
 						for (int i = tmp_range_right; i <= tmp_range_left; i++) {
@@ -4820,6 +4762,9 @@ void AstNode::mem2reg_as_needed_pass1(dict<AstNode*, pool<std::string>> &mem2reg
 	{
 		AstNode *mem = id2ast;
 
+		if (integer < (unsigned)mem->unpacked_dimensions)
+			input_error("Insufficient number of array indices for %s.\n", log_id(str));
+
 		// flag if used after blocking assignment (in same proc)
 		if ((proc_flags[mem] & AstNode::MEM2REG_FL_EQ1) && !(mem2reg_candidates[mem] & AstNode::MEM2REG_FL_EQ2)) {
 			mem2reg_places[mem].insert(stringf("%s:%d", RTLIL::encode_filename(filename).c_str(), location.first_line));
@@ -5103,7 +5048,7 @@ bool AstNode::mem2reg_as_needed_pass2(pool<AstNode*> &mem2reg_set, AstNode *mod,
 				int width;
 				if (bit_part_sel)
 				{
-					bit_part_sel->dumpAst(nullptr, "? ");
+					// bit_part_sel->dumpAst(nullptr, "? ");
 					if (bit_part_sel->children.size() == 1)
 						width = 0;
 					else
diff --git a/frontends/verilog/verilog_parser.y b/frontends/verilog/verilog_parser.y
index 039e83491..244e24f31 100644
--- a/frontends/verilog/verilog_parser.y
+++ b/frontends/verilog/verilog_parser.y
@@ -197,9 +197,20 @@ static AstNode *checkRange(AstNode *type_node, AstNode *range_node)
 			range_node = makeRange(type_node->range_left, type_node->range_right, false);
 		}
 	}
-	if (range_node && range_node->children.size() != 2) {
-		frontend_verilog_yyerror("wire/reg/logic packed dimension must be of the form: [<expr>:<expr>], [<expr>+:<expr>], or [<expr>-:<expr>]");
+
+	if (range_node) {
+		bool valid = true;
+		if (range_node->type == AST_RANGE) {
+			valid = range_node->children.size() == 2;
+		} else {  // AST_MULTIRANGE
+			for (auto child : range_node->children) {
+				valid = valid && child->children.size() == 2;
+			}
+		}
+		if (!valid)
+			frontend_verilog_yyerror("wire/reg/logic packed dimension must be of the form [<expr>:<expr>]");
 	}
+
 	return range_node;
 }
 
@@ -672,7 +683,7 @@ module_arg:
 		ast_stack.back()->children.push_back(astbuf2);
 		delete astbuf1; // really only needed if multiple instances of same type.
 	} module_arg_opt_assignment |
-	attr wire_type range TOK_ID {
+	attr wire_type range_or_multirange TOK_ID {
 		AstNode *node = $2;
 		node->str = *$4;
 		SET_AST_NODE_LOC(node, @4, @4);
@@ -1165,7 +1176,7 @@ task_func_args:
 	task_func_port | task_func_args ',' task_func_port;
 
 task_func_port:
-	attr wire_type range {
+	attr wire_type range_or_multirange {
 		bool prev_was_input = true;
 		bool prev_was_output = false;
 		if (albuf) {
@@ -1928,7 +1939,7 @@ struct_var: TOK_ID	{	auto *var_node = astbuf2->clone();
 /////////
 
 wire_decl:
-	attr wire_type range {
+	attr wire_type range_or_multirange {
 		albuf = $1;
 		astbuf1 = $2;
 		astbuf2 = checkRange(astbuf1, $3);
@@ -2104,7 +2115,7 @@ type_name: TOK_ID		// first time seen
 	 ;
 
 typedef_decl:
-	TOK_TYPEDEF typedef_base_type range type_name range_or_multirange ';' {
+	TOK_TYPEDEF typedef_base_type range_or_multirange type_name range_or_multirange ';' {
 		astbuf1 = $2;
 		astbuf2 = checkRange(astbuf1, $3);
 		if (astbuf2)
diff --git a/tests/sat/sizebits.sv b/tests/sat/sizebits.sv
index 87fa08f89..de2e7ecc1 100644
--- a/tests/sat/sizebits.sv
+++ b/tests/sat/sizebits.sv
@@ -16,17 +16,17 @@ assert property ($size({3{x}}) == 3*4);
 assert property ($size(y) == 6);
 assert property ($size(y, 1) == 6);
 assert property ($size(y, (1+1)) == 4);
+assert property ($size(y[2], 1) == 4);
 // This is unsupported at the moment
-//assert property ($size(y[2], 1) == 4);
 //assert property ($size(y[2][1], 1) == 1);
 
 assert property ($size(z) == 6);
 assert property ($size(z, 1) == 6);
 assert property ($size(z, 2) == 8);
 assert property ($size(z, 3) == 4);
-// This is unsupported at the moment
 assert property ($size(z[3], 1) == 8);
 assert property ($size(z[3][3], 1) == 4);
+// This is unsupported at the moment
 //assert property ($size(z[3][3][3], 1) == 1);
 // This should trigger an error if enabled (it does).
 //assert property ($size(z, 4) == 4);
diff --git a/tests/svtypes/multirange_array.sv b/tests/svtypes/multirange_array.sv
index be0d3dfc2..29e4559ab 100644
--- a/tests/svtypes/multirange_array.sv
+++ b/tests/svtypes/multirange_array.sv
@@ -8,9 +8,21 @@ module top;
 	logic a [3];
 	logic b [3][5];
 	logic c [3][5][7];
+	logic [2:0] d;
+	logic [2:0][4:0] e;
+	logic [2:0][4:0][6:0] f;
+	logic [2:0] g [3];
+	logic [2:0][4:0] h [3][5];
+	logic [2:0][4:0][6:0] i [3][5][7];
 
 	`STATIC_ASSERT($bits(a) == 3);
 	`STATIC_ASSERT($bits(b) == 15);
 	`STATIC_ASSERT($bits(c) == 105);
+	`STATIC_ASSERT($bits(d) == 3);
+	`STATIC_ASSERT($bits(e) == 15);
+	`STATIC_ASSERT($bits(f) == 105);
+	`STATIC_ASSERT($bits(g) == 9);
+	`STATIC_ASSERT($bits(h) == 225);
+	`STATIC_ASSERT($bits(i) == 11025);
 
 endmodule