Generate placeholder code for "mark in.dst".
diff --git a/cmd/puffs-c/internal/cgen/statement.go b/cmd/puffs-c/internal/cgen/statement.go
index 4995671..09118fd 100644
--- a/cmd/puffs-c/internal/cgen/statement.go
+++ b/cmd/puffs-c/internal/cgen/statement.go
@@ -68,6 +68,10 @@
 		b.writes(";\n")
 		return nil
 
+	case a.KIO:
+		b.writes("/* placeholder */\n")
+		return nil
+
 	case a.KIf:
 		// TODO: for writeSuspendibles, make sure that we get order of
 		// sub-expression evaluation correct.
diff --git a/doc/puffs-the-language.md b/doc/puffs-the-language.md
index 01654a8..6a3975f 100644
--- a/doc/puffs-the-language.md
+++ b/doc/puffs-the-language.md
@@ -54,9 +54,10 @@
 
 - `var`
 
-1 keyword deals with I/O:
+2 keywords deal with I/O:
 
 - `limit`
+- `mark`
 
 TODO: categorize and, or, not, as, ref, deref, false, true, in, out, this, u8,
 u16, etc.
diff --git a/gen/c/std/flate.c b/gen/c/std/flate.c
index 696e280..457ccc4 100644
--- a/gen/c/std/flate.c
+++ b/gen/c/std/flate.c
@@ -831,6 +831,7 @@
   switch (coro_susp_point) {
     PUFFS_BASE__COROUTINE_SUSPENSION_POINT(0);
 
+    /* placeholder */
     while (true) {
       PUFFS_BASE__COROUTINE_SUSPENSION_POINT(1);
       if (a_dst.buf) {
diff --git a/lang/ast/ast.go b/lang/ast/ast.go
index 7146313..33ab789 100644
--- a/lang/ast/ast.go
+++ b/lang/ast/ast.go
@@ -40,6 +40,7 @@
 	KField
 	KFile
 	KFunc
+	KIO
 	KIf
 	KIterate
 	KJump
@@ -71,6 +72,7 @@
 	KField:     "KField",
 	KFile:      "KFile",
 	KFunc:      "KFunc",
+	KIO:        "KIO",
 	KIf:        "KIf",
 	KIterate:   "KIterate",
 	KJump:      "KJump",
@@ -133,6 +135,7 @@
 func (n *Node) Field() *Field         { return (*Field)(n) }
 func (n *Node) File() *File           { return (*File)(n) }
 func (n *Node) Func() *Func           { return (*Func)(n) }
+func (n *Node) IO() *IO               { return (*IO)(n) }
 func (n *Node) If() *If               { return (*If)(n) }
 func (n *Node) Iterate() *Iterate     { return (*Iterate)(n) }
 func (n *Node) Jump() *Jump           { return (*Jump)(n) }
@@ -480,6 +483,27 @@
 	}
 }
 
+// IO is "mark LHS":
+//  - ID0:   <IDMark>
+//  - LHS:   <Expr>
+//
+// TODO: "mark LHS { List2 }"?
+//
+// TODO: also represent "limit LHS"?
+type IO Node
+
+func (n *IO) Node() *Node   { return (*Node)(n) }
+func (n *IO) Keyword() t.ID { return n.id0 }
+func (n *IO) Value() *Expr  { return n.lhs.Expr() }
+
+func NewIO(keyword t.ID, value *Expr) *IO {
+	return &IO{
+		kind: KIO,
+		id0:  keyword,
+		lhs:  value.Node(),
+	}
+}
+
 // Return is "return LHS", "return error ID1" or "return suspension ID1":
 //  - ID0:   <0|IDError|IDSuspension>
 //  - ID1:   message
diff --git a/lang/check/bounds.go b/lang/check/bounds.go
index 18ff1f3..c517f2d 100644
--- a/lang/check/bounds.go
+++ b/lang/check/bounds.go
@@ -241,6 +241,10 @@
 		_, _, err := q.bcheckExpr(n.Expr(), 0)
 		return err
 
+	case a.KIO:
+		_, _, err := q.bcheckExpr(n.IO().Value(), 0)
+		return err
+
 	case a.KIf:
 		return q.bcheckIf(n.If())
 
diff --git a/lang/check/type.go b/lang/check/type.go
index 38ee599..df4bb09 100644
--- a/lang/check/type.go
+++ b/lang/check/type.go
@@ -84,6 +84,18 @@
 	case a.KExpr:
 		return q.tcheckExpr(n.Expr(), 0)
 
+	case a.KIO:
+		n := n.IO()
+		val := n.Value()
+		if err := q.tcheckExpr(val, 0); err != nil {
+			return err
+		}
+		typ := val.MType()
+		if key := typ.Name().Key(); typ.Decorator() != 0 || (key != t.KeyReader1 && key != t.KeyWriter1) {
+			return fmt.Errorf("check: %s expression %q, of type %q, does not have an I/O type",
+				n.Keyword().String(q.tm), val.String(q.tm), typ.String(q.tm))
+		}
+
 	case a.KIf:
 		for n := n.If(); n != nil; n = n.ElseIf() {
 			cond := n.Condition()
diff --git a/lang/parse/parse.go b/lang/parse/parse.go
index ab0ce69..ef29acf 100644
--- a/lang/parse/parse.go
+++ b/lang/parse/parse.go
@@ -632,6 +632,14 @@
 		o, err := p.parseIf()
 		return o.Node(), err
 
+	case t.KeyMark:
+		p.src = p.src[1:]
+		value, err := p.parseExpr()
+		if err != nil {
+			return nil, err
+		}
+		return a.NewIO(x, value).Node(), nil
+
 	case t.KeyReturn:
 		p.src = p.src[1:]
 		keyword, message, value, err := t.ID(0), t.ID(0), (*a.Expr)(nil), error(nil)
diff --git a/lang/token/list.go b/lang/token/list.go
index a11499d..a6c90fc 100644
--- a/lang/token/list.go
+++ b/lang/token/list.go
@@ -242,6 +242,7 @@
 	KeyConst      = Key(IDConst >> KeyShift)
 	KeyTry        = Key(IDTry >> KeyShift)
 	KeyIterate    = Key(IDIterate >> KeyShift)
+	KeyMark       = Key(IDMark >> KeyShift)
 
 	KeyFalse = Key(IDFalse >> KeyShift)
 	KeyTrue  = Key(IDTrue >> KeyShift)
@@ -428,6 +429,7 @@
 	IDConst      = ID(0x77<<KeyShift | FlagsOther)
 	IDTry        = ID(0x78<<KeyShift | FlagsOther)
 	IDIterate    = ID(0x79<<KeyShift | FlagsOther)
+	IDMark       = ID(0x7A<<KeyShift | FlagsOther)
 
 	IDFalse = ID(0x80<<KeyShift | FlagsLiteral | FlagsImplicitSemicolon)
 	IDTrue  = ID(0x81<<KeyShift | FlagsLiteral | FlagsImplicitSemicolon)
@@ -630,6 +632,7 @@
 	KeyConst:      {"const", IDConst},
 	KeyTry:        {"try", IDTry},
 	KeyIterate:    {"iterate", IDIterate},
+	KeyMark:       {"mark", IDMark},
 
 	KeyFalse: {"false", IDFalse},
 	KeyTrue:  {"true", IDTrue},
diff --git a/std/flate/decode_flate.puffs b/std/flate/decode_flate.puffs
index 2aaec98..427f3f5 100644
--- a/std/flate/decode_flate.puffs
+++ b/std/flate/decode_flate.puffs
@@ -116,6 +116,11 @@
 )
 
 pub func flate_decoder.decode?(dst writer1, src reader1)() {
+	// TODO: when is the mark undone? Explicitly or implicitly? Right now, we
+	// just assume that the mark is dropped at function end, since the arg is a
+	// writer1 not a *writer1. Should we support nested marks? Should this be
+	// "mark dst { etc }" that has a block and unmarks at the block end?
+	mark in.dst
 	while true {
 		var z status = try this.decode_blocks?(dst:in.dst, src:in.src)
 		if not z.is_suspension() {