lang: allow copy_from_slice arg to be read-only
diff --git a/lang/builtin/builtin.go b/lang/builtin/builtin.go
index 88febcf..25e08e2 100644
--- a/lang/builtin/builtin.go
+++ b/lang/builtin/builtin.go
@@ -912,16 +912,20 @@
 // "table T" types. After tokenizing (but before parsing) these XxxFunc strings
 // (e.g. in the lang/check package), replace "T1" and "T2" with "†" or "‡"
 // daggers, to avoid collision with a user-defined "T1" or "T2" type.
+//
+// Ditto for "R1" / "ρ", for a read-only flavor of "T1" / "†".
 
 const (
-	genericOldName1 = t.IDT1
-	genericOldName2 = t.IDT2
-	genericNewName1 = t.IDDagger1
-	genericNewName2 = t.IDDagger2
+	genericOldName1   = t.IDT1
+	genericOldName2   = t.IDT2
+	genericOldNameRo1 = t.IDR1
+	genericNewName1   = t.IDDagger1
+	genericNewName2   = t.IDDagger2
+	genericNewNameRo1 = t.IDRho1
 )
 
 var SliceFuncs = []string{
-	"GENERIC T1.copy_from_slice!(s: T1) u64",
+	"GENERIC T1.copy_from_slice!(s: R1) u64",
 	"GENERIC T1.length() u64",
 	"GENERIC T1.prefix(up_to: u64) T1",
 	"GENERIC T1.uintptr_low_12_bits() u32[..= 4095]",
@@ -997,10 +1001,13 @@
 
 	if generic {
 		for i := range tokens {
-			if id := tokens[i].ID; id == genericOldName1 {
-				tokens[i].ID = genericNewName1
+			tok := &tokens[i]
+			if id := tok.ID; id == genericOldName1 {
+				tok.ID = genericNewName1
 			} else if id == genericOldName2 {
-				tokens[i].ID = genericNewName2
+				tok.ID = genericNewName2
+			} else if id == genericOldNameRo1 {
+				tok.ID = genericNewNameRo1
 			}
 		}
 	}
diff --git a/lang/check/resolve.go b/lang/check/resolve.go
index 70ca2ae..4d9ade9 100644
--- a/lang/check/resolve.go
+++ b/lang/check/resolve.go
@@ -32,6 +32,7 @@
 var (
 	typeExprGeneric1    = a.NewTypeExpr(0, t.IDBase, t.IDDagger1, nil, nil, nil)
 	typeExprGeneric2    = a.NewTypeExpr(0, t.IDBase, t.IDDagger2, nil, nil, nil)
+	typeExprGenericRo1  = a.NewTypeExpr(0, t.IDBase, t.IDRho1, nil, nil, nil)
 	typeExprIdeal       = a.NewTypeExpr(0, t.IDBase, t.IDQIdeal, nil, nil, nil)
 	typeExprList        = a.NewTypeExpr(0, t.IDBase, t.IDComma, nil, nil, nil)
 	typeExprNonNullptr  = a.NewTypeExpr(0, t.IDBase, t.IDQNonNullptr, nil, nil, nil)
diff --git a/lang/check/type.go b/lang/check/type.go
index 9b9e0b7..07eb7a3 100644
--- a/lang/check/type.go
+++ b/lang/check/type.go
@@ -624,16 +624,24 @@
 
 	genericType1 := (*a.TypeExpr)(nil)
 	genericType2 := (*a.TypeExpr)(nil)
+	genericTypeRo1 := (*a.TypeExpr)(nil)
 	if recv := f.Receiver(); recv[0] == t.IDBase {
 		switch recv[1] {
 		case t.IDDagger1:
+			decorator := t.ID(0)
 			genericType1 = lhs.MType().Receiver()
+			if gt1Dec := genericType1.Decorator(); (gt1Dec == t.IDRoslice) || (gt1Dec == t.IDSlice) {
+				decorator = t.IDRoslice
+			} else {
+				return fmt.Errorf("check: internal error: %q is not a generic slice", genericType1.Str(q.tm))
+			}
+			genericTypeRo1 = a.NewTypeExpr(decorator, 0, 0, nil, nil, genericType1.Inner())
 		case t.IDDagger2:
 			decorator := t.ID(0)
 			genericType2 = lhs.MType().Receiver()
-			if genericType2.Decorator() == t.IDRotable {
+			if gt2Dec := genericType2.Decorator(); gt2Dec == t.IDRotable {
 				decorator = t.IDRoslice
-			} else if genericType2.Decorator() == t.IDTable {
+			} else if gt2Dec == t.IDTable {
 				decorator = t.IDSlice
 			} else {
 				return fmt.Errorf("check: internal error: %q is not a generic table", genericType2.Str(q.tm))
@@ -664,6 +672,8 @@
 			inFieldTyp = genericType1
 		} else if genericType2 != nil && inFieldTyp.Eq(typeExprGeneric2) {
 			inFieldTyp = genericType2
+		} else if genericTypeRo1 != nil && inFieldTyp.Eq(typeExprGenericRo1) {
+			inFieldTyp = genericTypeRo1
 		}
 		if err := q.tcheckEq(inField.Name(), nil, inFieldTyp, o.Value(), o.Value().MType()); err != nil {
 			return err
@@ -1149,7 +1159,8 @@
 			// TODO: reject. You can only refine numeric types.
 		}
 		if qid[0] == t.IDBase {
-			if _, ok := builtInTypeMap[qid[1]]; ok || qid[1] == t.IDDagger1 || qid[1] == t.IDDagger2 {
+			if _, ok := builtInTypeMap[qid[1]]; ok ||
+				qid[1] == t.IDDagger1 || qid[1] == t.IDDagger2 || qid[1] == t.IDRho1 {
 				break swtch
 			}
 		}
diff --git a/lang/token/list.go b/lang/token/list.go
index 2ace176..dc45b72 100644
--- a/lang/token/list.go
+++ b/lang/token/list.go
@@ -439,10 +439,12 @@
 	IDCoroutineResumed = ID(0x101)
 	IDThis             = ID(0x102)
 
-	IDT1      = ID(0x104)
-	IDT2      = ID(0x105)
-	IDDagger1 = ID(0x106)
-	IDDagger2 = ID(0x107)
+	IDR1      = ID(0x104)
+	IDT1      = ID(0x105)
+	IDT2      = ID(0x106)
+	IDRho1    = ID(0x107)
+	IDDagger1 = ID(0x108)
+	IDDagger2 = ID(0x109)
 
 	IDQNonNullptr  = ID(0x10A)
 	IDQNullptr     = ID(0x10B)
@@ -851,10 +853,13 @@
 	// specifically non-ASCII so that no user-defined (non built-in) identifier
 	// will conflict with them.
 
-	// IDDaggerN is used by the type checker as a placeholder built-in ID to
-	// represent a generic type.
+	// IDRhoN and IDDaggerN are used by the type checker as placeholder
+	// built-in IDs to represent generic 1-dimensional (slice) or 2-dimensional
+	// (table) types. The Rho flavor is read-only.
+	IDR1:      "R1",
 	IDT1:      "T1",
 	IDT2:      "T2",
+	IDRho1:    "ρ", // U+03C1 GREEK SMALL LETTER RHO
 	IDDagger1: "†", // U+2020 DAGGER
 	IDDagger2: "‡", // U+2021 DOUBLE DAGGER