Three promises
A global value may be kept. A local one belongs to a region and must not outlive it. A parameter's mode is what the function promises about its argument:
@ global: the function may keep it.@ linked: the function does not keep it, but the result may hold it, so the result is as local as the argument.@ local: the function neither keeps it nor returns it.An unmarked parameter is
@ linked. An unmarked result is global, apart from what it holds of linked arguments.
type Note = {
text: String;
}
type Kept = {
text: String @ global;
}
Results follow their arguments
A linked argument may come back in the result, as itself or inside a new value. A new value lives where its parts do, so the function may build it anywhere in its body and return it. A global note passed in gives a result the caller may keep.
let passpass: fn(note: 'a) -> 'a = fn(notenote: 'a) { notenote: 'a };
let copycopy: fn(note: Book.LanguageReference.Locality.Note) -> Book.LanguageReference.Locality.Note = fn(notenote: Book.LanguageReference.Locality.Note: Note) {
let copiedcopied: Book.LanguageReference.Locality.Note = Note { text = notenote: Book.LanguageReference.Locality.Note.texttext: String; };
copiedcopied: Book.LanguageReference.Locality.Note
};
let spelledspelled: fn(note: Book.LanguageReference.Locality.Note) -> Book.LanguageReference.Locality.Note = fn(notenote: Book.LanguageReference.Locality.Note: Note @ linked) { notenote: Book.LanguageReference.Locality.Note };
Result Follows The Argument
✓copycopy: fn(note: Book.LanguageReference.Locality.Note) -> Book.LanguageReference.Locality.Note: fn(note: Note) -> Note;
✓spelledspelled: fn(note: Book.LanguageReference.Locality.Note) -> Book.LanguageReference.Locality.Note: fn(note: Note) -> Note;
let notenote: Book.LanguageReference.Locality.Note = Note { text = "ada""ada": String; };
let keptkept: Book.LanguageReference.Locality.Kept = Kept { text = copycopy: fn(note: Book.LanguageReference.Locality.Note) -> Book.LanguageReference.Locality.Note(notenote: Book.LanguageReference.Locality.Note).texttext: String; };
expect ✓(passpass: fn(note: Book.LanguageReference.Locality.Note) -> Book.LanguageReference.Locality.Note(notenote: Book.LanguageReference.Locality.Note).texttext: String, spelledspelled: fn(note: Book.LanguageReference.Locality.Note) -> Book.LanguageReference.Locality.Note(notenote: Book.LanguageReference.Locality.Note).texttext: String, keptkept: Book.LanguageReference.Locality.Kept.texttext: String) == ("ada""ada": String, "ada""ada": String, "ada""ada": String);Local parameters
@ local promises more: the result does not hold the argument, so callers know it is independent of what they passed. return! builds a result in the caller anyway, from values that already live there, and makes it @ local whatever the arguments are. A local result passes through return! again.
let untitleduntitled: fn(note: Book.LanguageReference.Locality.Note @ local) -> Bool = fn(notenote: Book.LanguageReference.Locality.Note: Note @ local) { notenote: Book.LanguageReference.Locality.Note.texttext: String == """": String };
let local_copylocal_copy: fn(note: Book.LanguageReference.Locality.Note @ local) -> Book.LanguageReference.Locality.Note @ local = fn(notenote: Book.LanguageReference.Locality.Note: Note @ local) {
return! Note { text = notenote: Book.LanguageReference.Locality.Note.texttext: String; };
};
let recopyrecopy: fn(note: Book.LanguageReference.Locality.Note @ local) -> Book.LanguageReference.Locality.Note @ local = fn(notenote: Book.LanguageReference.Locality.Note: Note @ local) {
return! local_copylocal_copy: fn(note: Book.LanguageReference.Locality.Note @ local) -> Book.LanguageReference.Locality.Note @ local(notenote: Book.LanguageReference.Locality.Note);
};
Local Promises
✓local_copylocal_copy: fn(note: Book.LanguageReference.Locality.Note @ local) -> Book.LanguageReference.Locality.Note @ local: fn(note: Note @ local) -> Note @ local;
let notenote: Book.LanguageReference.Locality.Note = Note { text = "ada""ada": String; };
expect ✓(untitleduntitled: fn(note: Book.LanguageReference.Locality.Note @ local) -> Bool(notenote: Book.LanguageReference.Locality.Note), local_copylocal_copy: fn(note: Book.LanguageReference.Locality.Note @ local) -> Book.LanguageReference.Locality.Note @ local(notenote: Book.LanguageReference.Locality.Note).texttext: String, recopyrecopy: fn(note: Book.LanguageReference.Locality.Note @ local) -> Book.LanguageReference.Locality.Note @ local(notenote: Book.LanguageReference.Locality.Note).texttext: String) == (FalseFalse: Bool, "ada""ada": String, "ada""ada": String);Errors follow their arguments
An error leaves a function the way a result does: it may hold linked arguments, it lives in the caller, and each function it passes checks it again.
let requirerequire: fn(note: Book.LanguageReference.Locality.Note) -> Book.LanguageReference.Locality.Note, exn [ :empty(Book.LanguageReference.Locality.Note) ] = fn(notenote: Book.LanguageReference.Locality.Note: Note) -> Note {
if notenote: Book.LanguageReference.Locality.Note.texttext: String == """": String {
error :empty(notenote: Book.LanguageReference.Locality.Note);
}
notenote: Book.LanguageReference.Locality.Note
};
let describedescribe: fn(note: Book.LanguageReference.Locality.Note) -> String = fn(notenote: Book.LanguageReference.Locality.Note) {
match requirerequire: fn(note: Book.LanguageReference.Locality.Note) -> Book.LanguageReference.Locality.Note, exn [ :empty(Book.LanguageReference.Locality.Note) ](notenote: Book.LanguageReference.Locality.Note) {
| foundfound: Book.LanguageReference.Locality.Note -> foundfound: Book.LanguageReference.Locality.Note.texttext: String
| error :empty(missingmissing: Book.LanguageReference.Locality.Note) -> "(""(": String ++ missingmissing: Book.LanguageReference.Locality.Note.texttext: String ++ ")"")": String
}
};
Errors Follow Their Arguments
expect ✓(
describedescribe: fn(note: Book.LanguageReference.Locality.Note) -> String(Note { text = "ada""ada": String; }),
describedescribe: fn(note: Book.LanguageReference.Locality.Note) -> String(Note { text = """": String; }),
) == ("ada""ada": String, "()""()": String);Pinned fields
A field pinned @ global holds a global value even inside a local one. Projecting it, or destructuring it at any depth, gives a value that may be kept.
type Session = {
id: String @ global;
draft: String;
}
let session_idsession_id: fn(session: Book.LanguageReference.Locality.Session @ local) -> Book.LanguageReference.Locality.Kept = fn(sessionsession: Book.LanguageReference.Locality.Session: Session @ local) { Kept { text = sessionsession: Book.LanguageReference.Locality.Session.idid: String; } };
let destructured_iddestructured_id: fn(session: Book.LanguageReference.Locality.Session @ local) -> Book.LanguageReference.Locality.Kept = fn(sessionsession: Book.LanguageReference.Locality.Session: Session @ local) {
let { idid: String; ..; } = sessionsession: Book.LanguageReference.Locality.Session;
Kept { text = idid: String; }
};
let wrapped_idwrapped_id: fn(session: Book.LanguageReference.Locality.Session @ local) -> Book.LanguageReference.Locality.Kept = fn(sessionsession: Book.LanguageReference.Locality.Session: Session @ local) {
match SomeSome: fn(Book.LanguageReference.Locality.Session) -> Option(Book.LanguageReference.Locality.Session)(sessionsession: Book.LanguageReference.Locality.Session) {
| Some(innerinner: Book.LanguageReference.Locality.Session) -> Kept { text = innerinner: Book.LanguageReference.Locality.Session.idid: String; }
| None -> Kept { text = """": String; }
}
};
Pinned Field Stays Global
let sessionsession: Book.LanguageReference.Locality.Session = Session { id = "s1""s1": String; draft = "hi""hi": String; };
expect ✓(
session_idsession_id: fn(session: Book.LanguageReference.Locality.Session @ local) -> Book.LanguageReference.Locality.Kept(sessionsession: Book.LanguageReference.Locality.Session).texttext: String,
destructured_iddestructured_id: fn(session: Book.LanguageReference.Locality.Session @ local) -> Book.LanguageReference.Locality.Kept(sessionsession: Book.LanguageReference.Locality.Session).texttext: String,
wrapped_idwrapped_id: fn(session: Book.LanguageReference.Locality.Session @ local) -> Book.LanguageReference.Locality.Kept(sessionsession: Book.LanguageReference.Locality.Session).texttext: String,
) == ("s1""s1": String, "s1""s1": String, "s1""s1": String);Stores
A store keeps its value as long as the container lives. A container built here is new: it lives in the outermost region that holds it, so it may take any value that lives at least as long. Any other container may be global, and takes only global values.
type Draft = {
mut text: String;
}
let redraftredraft: fn(note: Book.LanguageReference.Locality.Note @ local, revised: Book.LanguageReference.Locality.Note @ local) -> Bool = fn(notenote: Book.LanguageReference.Locality.Note: Note @ local, revisedrevised: Book.LanguageReference.Locality.Note: Note @ local) {
let draftdraft: Book.LanguageReference.Locality.Draft = Draft { text = notenote: Book.LanguageReference.Locality.Note.texttext: String; };
draft.text := revisedrevised: Book.LanguageReference.Locality.Note.texttext: String;
draftdraft: Book.LanguageReference.Locality.Draft.texttext: String == revisedrevised: Book.LanguageReference.Locality.Note.texttext: String
};
Stores Into A Local Container
expect ✓redraftredraft: fn(note: Book.LanguageReference.Locality.Note @ local, revised: Book.LanguageReference.Locality.Note @ local) -> Bool(Note { text = "a""a": String; }, Note { text = "b""b": String; });A value reached only through one parameter may go back into that parameter: the parameter already holds it, so it lives long enough. advance keeps nothing.
type Cursor = {
items: Vector(String);
mut current: String;
}
let advanceadvance: fn(cursor: Book.LanguageReference.Locality.Cursor, index: Int) -> Unit = fn(cursorcursor: Book.LanguageReference.Locality.Cursor: Cursor, indexindex: Int: Int) {
cursor.current := Vector.getget: pure fn!('a)(items: Vector(String), index: Int) -> String(cursorcursor: Book.LanguageReference.Locality.Cursor.itemsitems: Vector(String), indexindex: Int);
};
Stores What A Parameter Already Holds
let cursorcursor: Book.LanguageReference.Locality.Cursor = Cursor { items = ["ada""ada": String, "lin""lin": String]; current = """": String; };
advanceadvance: fn(cursor: Book.LanguageReference.Locality.Cursor, index: Int) -> Unit(cursorcursor: Book.LanguageReference.Locality.Cursor, 11: Int);
expect ✓cursorcursor: Book.LanguageReference.Locality.Cursor.currentcurrent: String == "lin""lin": String;Inferred parameters
A parameter without a mode is inferred. One the body keeps, directly, through a closure it stores, or in a task it spawns, becomes @ global, and callers must pass a global value. Any other stays linked, so callers may pass anything.
type! Component = {
render: (fn(body: String) -> String) @ global;
}
let keep_textkeep_text: fn(text: String @ global) -> Book.LanguageReference.Locality.Kept = fn(texttext: String) { Kept { text = texttext: String; } };
let registerregister: fn(prefix: String @ global) -> Book.LanguageReference.Locality.Component = fn(prefixprefix: String) {
Component { render = (bodybody: String) => prefixprefix: String ++ bodybody: String; }
};
let laterlater: fn(text: String @ global) -> Async.Task(String) = fn(texttext: String) {
spawn {
texttext: String ++ "!""!": String
}
};
let is_blankis_blank: fn(text: String) -> Bool = fn(texttext: String) { texttext: String == """": String };
let blank_noteblank_note: fn(note: Book.LanguageReference.Locality.Note @ local) -> Bool = fn(notenote: Book.LanguageReference.Locality.Note: Note @ local) { is_blankis_blank: fn(text: String) -> Bool(notenote: Book.LanguageReference.Locality.Note.texttext: String) };
Inferred Retention
✓keep_textkeep_text: fn(text: String @ global) -> Book.LanguageReference.Locality.Kept: fn(text: String @ global) -> Kept;
let keptkept: Book.LanguageReference.Locality.Kept = keep_textkeep_text: fn(text: String @ global) -> Book.LanguageReference.Locality.Kept("ada""ada": String);
✓registerregister: fn(prefix: String @ global) -> Book.LanguageReference.Locality.Component: fn(prefix: String @ global) -> Component;
let componentcomponent: Book.LanguageReference.Locality.Component = registerregister: fn(prefix: String @ global) -> Book.LanguageReference.Locality.Component("> ""> ": String);
✓laterlater: fn(text: String @ global) -> Async.Task(String): fn(text: String @ global) -> Async.Task(String);
let tasktask: Async.Task(String) = laterlater: fn(text: String @ global) -> Async.Task(String)("lin""lin": String);
✓is_blankis_blank: fn(text: String) -> Bool: fn(text: String) -> Bool;
expect ✓(
keptkept: Book.LanguageReference.Locality.Kept.texttext: String,
componentcomponent: Book.LanguageReference.Locality.Component.renderrender: fn(body: String) -> String("hi""hi": String),
await tasktask: Async.Task(String),
blank_noteblank_note: fn(note: Book.LanguageReference.Locality.Note @ local) -> Bool(Note { text = """": String; }),
) == ("ada""ada": String, "> hi""> hi": String, "lin!""lin!": String, TrueTrue: Bool);Callbacks
A function passed or stored as a value is known only by its type. A callback type's unmarked parameter is linked: the callback may return it, inside a new value or not, but not keep it. Only a slot marked @ global accepts a callback that keeps its argument, and passing a value into that slot keeps it.
let remember_withremember_with: fn(text: String @ global, remember: fn(text: String @ global) -> Book.LanguageReference.Locality.Kept) -> Book.LanguageReference.Locality.Kept = fn(texttext: String, rememberremember: fn(text: String @ global) -> Book.LanguageReference.Locality.Kept: fn(text: String @ global) -> Kept) {
rememberremember: fn(text: String @ global) -> Book.LanguageReference.Locality.Kept(texttext: String)
};
let check_withcheck_with: fn(text: String, check: fn(text: String) -> Bool) -> Bool = fn(texttext: String, checkcheck: fn(text: String) -> Bool: fn(text: String) -> Bool) { checkcheck: fn(text: String) -> Bool(texttext: String) };
let titlestitles: fn(notes: Vector(Book.LanguageReference.Locality.Note)) -> Vector(Option(String)) = fn(notesnotes: Vector(Book.LanguageReference.Locality.Note): Vector(Note)) {
Vector.mapmap: pure fn!('a, 'b)(items: Vector(Book.LanguageReference.Locality.Note) @ 'm, ~f: fn(value: Book.LanguageReference.Locality.Note @ 'm) -> Option(String)) -> Vector(Option(String))(notesnotes: Vector(Book.LanguageReference.Locality.Note), f = (notenote: Book.LanguageReference.Locality.Note) => SomeSome: fn(String) -> Option(String)(notenote: Book.LanguageReference.Locality.Note.texttext: String))
};
Callbacks Say What They Keep
✓remember_withremember_with: fn(text: String @ global, remember: fn(text: String @ global) -> Book.LanguageReference.Locality.Kept) -> Book.LanguageReference.Locality.Kept: fn(
text: String @ global,
remember: fn(text: String @ global) -> Kept,
) -> Kept;
let keptkept: Book.LanguageReference.Locality.Kept = remember_withremember_with: fn(text: String @ global, remember: fn(text: String @ global) -> Book.LanguageReference.Locality.Kept) -> Book.LanguageReference.Locality.Kept("ada""ada": String, keep_textkeep_text: fn(text: String @ global) -> Book.LanguageReference.Locality.Kept);
let directdirect: Book.LanguageReference.Locality.Kept = remember_withremember_with: fn(text: String @ global, remember: fn(text: String @ global) -> Book.LanguageReference.Locality.Kept) -> Book.LanguageReference.Locality.Kept(
"lin""lin": String,
(texttext: String) =>
Kept { text = texttext: String; },
);
expect ✓(keptkept: Book.LanguageReference.Locality.Kept.texttext: String, directdirect: Book.LanguageReference.Locality.Kept.texttext: String, check_withcheck_with: fn(text: String, check: fn(text: String) -> Bool) -> Bool("""": String, is_blankis_blank: fn(text: String) -> Bool)) == ("ada""ada": String, "lin""lin": String, TrueTrue: Bool);
expect ✓titlestitles: fn(notes: Vector(Book.LanguageReference.Locality.Note)) -> Vector(Option(String))([Note { text = "ada""ada": String; }]) == [SomeSome: fn(String) -> Option(String)("ada""ada": String)];Mode variables
A mode variable names a region each call picks. Every argument written @ 'm lives at least as long as that region, and a result written @ 'm lives there. A container at 'm lives exactly there, so it takes any other value of 'm, local or global, but nothing that dies sooner. Array.push is written this way. Mode variables are written, never inferred.
let addadd: fn(note: Book.LanguageReference.Locality.Note @ 'm, notes: Array(Book.LanguageReference.Locality.Note) @ 'm) -> Unit = fn(notenote: Book.LanguageReference.Locality.Note: Note @ 'm, notesnotes: Array(Book.LanguageReference.Locality.Note): Array(Note) @ 'm) {
Array.pushpush: pure fn!('a)(items: Array(Book.LanguageReference.Locality.Note) @ 'm, item: Book.LanguageReference.Locality.Note @ 'm) -> Unit(notesnotes: Array(Book.LanguageReference.Locality.Note), notenote: Book.LanguageReference.Locality.Note);
};
let count_intocount_into: fn(note: Book.LanguageReference.Locality.Note @ local) -> Int = fn(notenote: Book.LanguageReference.Locality.Note: Note @ local) -> Int {
let notesnotes: Array(Book.LanguageReference.Locality.Note): Array(Note) = Array.newnew: pure fn!('a)() -> Array(Book.LanguageReference.Locality.Note) @ 'm();
addadd: fn(note: Book.LanguageReference.Locality.Note @ 'm, notes: Array(Book.LanguageReference.Locality.Note) @ 'm) -> Unit(notenote: Book.LanguageReference.Locality.Note, notesnotes: Array(Book.LanguageReference.Locality.Note));
Array.lengthlength: pure fn!('a)(items: Array(Book.LanguageReference.Locality.Note)) -> Int(notesnotes: Array(Book.LanguageReference.Locality.Note))
};
Arguments Share A Region
✓Array.pushpush: pure fn!('a)(items: Array('a) @ 'm, item: 'a @ 'm) -> Unit: pure fn!('a)(items: Array('a) @ 'm, item: 'a @ 'm) -> Unit;
✓addadd: fn(note: Book.LanguageReference.Locality.Note @ 'm, notes: Array(Book.LanguageReference.Locality.Note) @ 'm) -> Unit: fn(note: Note @ 'm, notes: Array(Note) @ 'm) -> Unit;
expect ✓count_intocount_into: fn(note: Book.LanguageReference.Locality.Note @ local) -> Int(Note { text = "ada""ada": String; }) == 11: Int;A fresh array lives in the outermost region that holds it. A function may fill one with linked arguments and return it; they come back with it.
let pairpair: fn(first: Book.LanguageReference.Locality.Note, second: Book.LanguageReference.Locality.Note) -> Array(Book.LanguageReference.Locality.Note) = fn(firstfirst: Book.LanguageReference.Locality.Note: Note, secondsecond: Book.LanguageReference.Locality.Note: Note) {
let notesnotes: Array(Book.LanguageReference.Locality.Note): Array(Note) = Array.newnew: pure fn!('a)() -> Array(Book.LanguageReference.Locality.Note) @ 'm();
Array.pushpush: pure fn!('a)(items: Array(Book.LanguageReference.Locality.Note) @ 'm, item: Book.LanguageReference.Locality.Note @ 'm) -> Unit(notesnotes: Array(Book.LanguageReference.Locality.Note), firstfirst: Book.LanguageReference.Locality.Note);
Array.pushpush: pure fn!('a)(items: Array(Book.LanguageReference.Locality.Note) @ 'm, item: Book.LanguageReference.Locality.Note @ 'm) -> Unit(notesnotes: Array(Book.LanguageReference.Locality.Note), secondsecond: Book.LanguageReference.Locality.Note);
notesnotes: Array(Book.LanguageReference.Locality.Note)
};
Fresh Arrays Leave With Their Contents
✓pairpair: fn(first: Book.LanguageReference.Locality.Note, second: Book.LanguageReference.Locality.Note) -> Array(Book.LanguageReference.Locality.Note): fn(first: Note, second: Note) -> Array(Note);
expect ✓Array.lengthlength: pure fn!('a)(items: Array(Book.LanguageReference.Locality.Note)) -> Int(
pairpair: fn(first: Book.LanguageReference.Locality.Note, second: Book.LanguageReference.Locality.Note) -> Array(Book.LanguageReference.Locality.Note)(Note { text = "ada""ada": String; }, Note { text = "lin""lin": String; }),
) == 22: Int;A callback parameter written with the callee's variable stands for every argument of that variable at the call. Vector.iter passes items at the vector's 'm, so a callback may keep the items of a global vector, or store a vector's items into a container of the same region.
let keep_allkeep_all: fn(notes: Vector(Book.LanguageReference.Locality.Note) @ global, into: Array(Book.LanguageReference.Locality.Note) @ global) -> Unit = fn(notesnotes: Vector(Book.LanguageReference.Locality.Note): Vector(Note) @ global, intointo: Array(Book.LanguageReference.Locality.Note): Array(Note) @ global) {
Vector.iteriter: pure fn!('a)(items: Vector(Book.LanguageReference.Locality.Note) @ 'm, ~f: fn(value: Book.LanguageReference.Locality.Note @ 'm) -> Unit) -> Unit(notesnotes: Vector(Book.LanguageReference.Locality.Note), f = (notenote: Book.LanguageReference.Locality.Note) => Array.pushpush: pure fn!('a)(items: Array(Book.LanguageReference.Locality.Note) @ 'm, item: Book.LanguageReference.Locality.Note @ 'm) -> Unit(intointo: Array(Book.LanguageReference.Locality.Note), notenote: Book.LanguageReference.Locality.Note));
};
let collectcollect: fn(notes: Vector(Book.LanguageReference.Locality.Note) @ 'm, acc: Array(Book.LanguageReference.Locality.Note) @ 'm) -> Unit = fn(notesnotes: Vector(Book.LanguageReference.Locality.Note): Vector(Note) @ 'm, accacc: Array(Book.LanguageReference.Locality.Note): Array(Note) @ 'm) {
Vector.iteriter: pure fn!('a)(items: Vector(Book.LanguageReference.Locality.Note) @ 'm, ~f: fn(value: Book.LanguageReference.Locality.Note @ 'm) -> Unit) -> Unit(notesnotes: Vector(Book.LanguageReference.Locality.Note), f = (notenote: Book.LanguageReference.Locality.Note) => Array.pushpush: pure fn!('a)(items: Array(Book.LanguageReference.Locality.Note) @ 'm, item: Book.LanguageReference.Locality.Note @ 'm) -> Unit(accacc: Array(Book.LanguageReference.Locality.Note), notenote: Book.LanguageReference.Locality.Note));
};
Callbacks Share The Call's Region
✓Vector.iteriter: pure fn!('a)(items: Vector('a) @ 'm, ~f: fn(value: 'a @ 'm) -> Unit) -> Unit: pure fn!('a)(items: Vector('a) @ 'm, ~f: fn(value: 'a @ 'm) -> Unit) -> Unit;
let notesnotes: Vector(Book.LanguageReference.Locality.Note) = [Note { text = "ada""ada": String; }, Note { text = "lin""lin": String; }];
let keptkept: Array(Book.LanguageReference.Locality.Note): Array(Note) = Array.newnew: pure fn!('a)() -> Array(Book.LanguageReference.Locality.Note) @ 'm();
let collectedcollected: Array(Book.LanguageReference.Locality.Note): Array(Note) = Array.newnew: pure fn!('a)() -> Array(Book.LanguageReference.Locality.Note) @ 'm();
keep_allkeep_all: fn(notes: Vector(Book.LanguageReference.Locality.Note) @ global, into: Array(Book.LanguageReference.Locality.Note) @ global) -> Unit(notesnotes: Vector(Book.LanguageReference.Locality.Note), keptkept: Array(Book.LanguageReference.Locality.Note));
collectcollect: fn(notes: Vector(Book.LanguageReference.Locality.Note) @ 'm, acc: Array(Book.LanguageReference.Locality.Note) @ 'm) -> Unit(notesnotes: Vector(Book.LanguageReference.Locality.Note), collectedcollected: Array(Book.LanguageReference.Locality.Note));
expect ✓(Array.lengthlength: pure fn!('a)(items: Array(Book.LanguageReference.Locality.Note)) -> Int(keptkept: Array(Book.LanguageReference.Locality.Note)), Array.lengthlength: pure fn!('a)(items: Array(Book.LanguageReference.Locality.Note)) -> Int(collectedcollected: Array(Book.LanguageReference.Locality.Note))) == (22: Int, 22: Int);Only a container itself takes values of its region: a value built here, a fresh array, a parameter at 'm, or a field it owns. Any other field, an element read back out, or a call's result may be reached from elsewhere, so storing into one takes only global values.
Owned fields
A field written @ unique is owned: its record holds the only reference to the field's value, so the value lives exactly as long as the record and takes what the record could. The record gets a fresh value there, built where it is constructed or returned fresh by a call like Array.new(), and an update replaces it. An element read out of an owned array is not owned. A generated decoder cannot build an owned field fresh, so such a record has no automatic Decode.
type Index = {
names: Array(String) @ unique;
mut size: Int;
}
let add_nameadd_name: fn(index: Book.LanguageReference.Locality.Index @ 'm, name: String @ 'm) -> Unit = fn(indexindex: Book.LanguageReference.Locality.Index: Index @ 'm, namename: String: String @ 'm) {
Array.pushpush: pure fn!('a)(items: Array(String) @ 'm, item: String @ 'm) -> Unit(indexindex: Book.LanguageReference.Locality.Index.namesnames: Array(String), namename: String);
index.size := indexindex: Book.LanguageReference.Locality.Index.sizesize: Int + 11: Int;
};
let count_namescount_names: fn(first: String @ local, second: String @ local) -> Int = fn(firstfirst: String: String @ local, secondsecond: String: String @ local) -> Int {
let indexindex: Book.LanguageReference.Locality.Index = Index { names = Array.newnew: pure fn!('a)() -> Array(String) @ 'm(); size = 00: Int; };
add_nameadd_name: fn(index: Book.LanguageReference.Locality.Index @ 'm, name: String @ 'm) -> Unit(indexindex: Book.LanguageReference.Locality.Index, firstfirst: String);
Array.pushpush: pure fn!('a)(items: Array(String) @ 'm, item: String @ 'm) -> Unit(indexindex: Book.LanguageReference.Locality.Index.namesnames: Array(String), secondsecond: String);
indexindex: Book.LanguageReference.Locality.Index.sizesize: Int + Array.lengthlength: pure fn!('a)(items: Array(String)) -> Int(indexindex: Book.LanguageReference.Locality.Index.namesnames: Array(String))
};
Owned Fields Live With Their Record
expect ✓count_namescount_names: fn(first: String @ local, second: String @ local) -> Int("ada""ada": String, "lin""lin": String) == 33: Int;Behavior members
Generic code calls a behavior's member through the operation's type, so a member may keep no more than that type says, and parameters it puts in one region must share a mode variable there. A member function takes the operation's modes on the parameters where it writes none. The companion .shc declares Collect with add: pure fn!(bag: 'bag @ 'm, name: String
@ 'm) -> Unit, so Shelf's member may store the name in the shelf.
type Shelf = {
names: Array(String) @ unique;
}
module behavior Collect(Shelf) {
addadd: fn(shelf: Book.LanguageReference.Locality.Shelf @ 'm, name: String @ 'm) -> Unit = (shelfshelf: Book.LanguageReference.Locality.Shelf: Shelf, namename: String: String) =>
Array.pushpush: pure fn!('a)(items: Array(String) @ 'm, item: String @ 'm) -> Unit(shelfshelf: Book.LanguageReference.Locality.Shelf.namesnames: Array(String), namename: String);
}
let shelveshelve: fn('bag: Collect)(bag: 'bag @ 'm, first: String @ 'm, second: String @ 'm) -> Unit = fn('bag: Collect)(bagbag: 'a: 'bag @ 'm, firstfirst: String: String @ 'm, secondsecond: String: String @ 'm) {
Collect.add(bagbag: 'a, firstfirst: String);
Collect.add(bagbag: 'a, secondsecond: String);
};
Members Keep What Their Operation Says
let shelfshelf: Book.LanguageReference.Locality.Shelf = Shelf { names = Array.newnew: pure fn!('a)() -> Array(String) @ 'm(); };
shelveshelve: fn('bag: Collect)(bag: Book.LanguageReference.Locality.Shelf @ 'm, first: String @ 'm, second: String @ 'm) -> Unit(shelfshelf: Book.LanguageReference.Locality.Shelf, "ada""ada": String, "lin""lin": String);
expect ✓Array.lengthlength: pure fn!('a)(items: Array(String)) -> Int(shelfshelf: Book.LanguageReference.Locality.Shelf.namesnames: Array(String)) == 22: Int;Written signatures
A let rec head is checked before its body, because the body calls it. Write @ global on every parameter the body keeps, including one that only reaches a kept parameter through the recursive call.
let rec rotaterotate: fn(first: String @ global, second: String @ global, turns: Int) -> Book.LanguageReference.Locality.Kept, exn [ 'a ], eff 'b = (firstfirst: String: String @ global, secondsecond: String: String @ global, turnsturns: Int: Int) -> Kept =>
if turnsturns: Int == 00: Int {
| Kept { text = firstfirst: String; }
| rotaterotate: fn(first: String @ global, second: String @ global, turns: Int) -> Book.LanguageReference.Locality.Kept, exn [ 'a ], eff 'b(secondsecond: String, firstfirst: String, turnsturns: Int - 11: Int)
};
Recursion Writes What It Keeps
expect ✓(rotaterotate: fn(first: String @ global, second: String @ global, turns: Int) -> Book.LanguageReference.Locality.Kept, exn [ 'a ], eff 'b("ada""ada": String, "lin""lin": String, 00: Int).texttext: String, rotaterotate: fn(first: String @ global, second: String @ global, turns: Int) -> Book.LanguageReference.Locality.Kept, exn [ 'a ], eff 'b("ada""ada": String, "lin""lin": String, 11: Int).texttext: String) == ("ada""ada": String, "lin""lin": String);Contracts
A contract publishes what its functions keep. A contract parameter without a mode is linked, whatever the body would infer, so the companion .shc writes @ global on remember's note and leaves title's alone.
let rememberremember: pure fn!(note: Book.LanguageReference.Locality.Note @ global) -> Book.LanguageReference.Locality.Kept = fn(notenote: Book.LanguageReference.Locality.Note: Note) { Kept { text = notenote: Book.LanguageReference.Locality.Note.texttext: String; } };
let titletitle: pure fn!(note: Book.LanguageReference.Locality.Note) -> String = fn(notenote: Book.LanguageReference.Locality.Note: Note) { notenote: Book.LanguageReference.Locality.Note.texttext: String };
Contracts Publish Retention
let notenote: Book.LanguageReference.Locality.Note = Note { text = "ada""ada": String; };
expect ✓(rememberremember: pure fn!(note: Book.LanguageReference.Locality.Note @ global) -> Book.LanguageReference.Locality.Kept(notenote: Book.LanguageReference.Locality.Note).texttext: String, titletitle: pure fn!(note: Book.LanguageReference.Locality.Note) -> String(notenote: Book.LanguageReference.Locality.Note)) == ("ada""ada": String, "ada""ada": String);What locality rejects
Keeping a local value, returning one born in the function, returning a @ local parameter, and keeping or returning more than a signature, callback slot, or contract says are errors. So are storing a value into a container that may outlive it, keeping a parameter at 'm, and a callback keeping or storing what a call passes it at 'm when the call's arguments may die sooner.
Destructuring, match arms, reassignment, field stores, closure calls, local bindings, and spawn captures all carry a local value's region.
type Note = {
text: String;
}
type Kept = {
text: String @ global;
}
type Box = {
mut text: String;
}
let destructure = fn(note: Note @ local) {
let { text; } = note;
Kept { text = text; }
};
let arm = fn(note: Note @ local) {
match note {
| Note { text; } -> Kept { text = text; }
}
};
let reassign = fn(note: Note @ local) {
let mut text = "";
text := note.text;
Kept { text = text; }
};
let store = fn(note: Note @ local) {
let box = Box { text = ""; };
box.text := note.text;
box
};
let call = fn(note: Note @ local) {
let read = () => note.text;
Kept { text = read(); }
};
let annotated = fn() {
let note: Note @ local = Note { text = "ada"; };
Kept { text = note.text; }
};
let task = fn(note: Note @ local) {
let text = note.text;
spawn {
text
}
};
$ship check laundering/src/main.shp
error: `text` is global and this value is local
--> laundering/src/main.shp:15:17
|
15 | Kept { text = text; }
| ^^^^ local value cannot be kept
help: store a global value in this field, or remove `@ global` if the field should follow the container
error: `text` is global and this value is local
--> laundering/src/main.shp:20:39
|
20 | | Note { text; } -> Kept { text = text; }
| ^^^^ local value cannot be kept
help: store a global value in this field, or remove `@ global` if the field should follow the container
error: `text` is global and this value is local
--> laundering/src/main.shp:27:17
|
27 | Kept { text = text; }
| ^^^^ local value cannot be kept
help: store a global value in this field, or remove `@ global` if the field should follow the container
error: `note` is `@ local`, so the result cannot contain it
--> laundering/src/main.shp:33:3
|
33 | box
| ^^^ returned here
help: `@ local` promises the result does not hold this parameter: leave it unmarked (`@ linked`) to return it, or write `@ local` on the result
error: `text` is global and this value is local
--> laundering/src/main.shp:38:17
|
38 | Kept { text = read(); }
| ^^^^^^ local value cannot be kept
help: store a global value in this field, or remove `@ global` if the field should follow the container
error: `text` is global and this value is local
--> laundering/src/main.shp:43:17
|
43 | Kept { text = note.text; }
| ^^^^^^^^^ local value cannot be kept
help: store a global value in this field, or remove `@ global` if the field should follow the container
error: `spawn` cannot capture local binding `text`
--> laundering/src/main.shp:49:5
|
49 | text
| ^^^^ spawned tasks may outlive this binding
help: `spawn` is local event-loop work, not a Send boundary, but it is still an escape boundaryA stored slot pinned @ global needs a global value, whether the pin is on the field, on a generic slot, on the type parameter, or on a positional payload. A closure that captures a local is itself local.
type Note = {
text: String;
}
type Cache('a @ global) = {
value: 'a;
}
type Held('a) = {
held: 'a @ global;
}
type Slot =
| Pinned(String @ global)
| Empty
type! Component = {
render: (fn(body: String) -> String) @ global;
}
let cache = fn(note: Note @ local) { Cache { value = note.text; } };
let held = fn(note: Note @ local) { Held { held = note.text; } };
let pinned = fn(note: Note @ local) { Pinned(note.text) };
let component = fn(note: Note @ local) {
Component { render = (body) => note.text ++ body; }
};
$ship check cuts/src/main.shp
error: `value` is global and this value is local
--> cuts/src/main.shp:21:54
|
21 | let cache = fn(note: Note @ local) { Cache { value = note.text; } };
| ^^^^^^^^^ local value cannot be kept
help: store a global value in this field, or remove `@ global` if the field should follow the container
error: `held` is global and this value is local
--> cuts/src/main.shp:22:51
|
22 | let held = fn(note: Note @ local) { Held { held = note.text; } };
| ^^^^^^^^^ local value cannot be kept
help: store a global value in this field, or remove `@ global` if the field should follow the container
error: this payload is global and this value is local
--> cuts/src/main.shp:23:46
|
23 | let pinned = fn(note: Note @ local) { Pinned(note.text) };
| ^^^^^^^^^ local value cannot be kept
help: store a global value in this payload, or remove `@ global` if the payload should follow the value
error: `render` is global and this value is local
--> cuts/src/main.shp:26:24
|
26 | Component { render = (body) => note.text ++ body; }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ local value cannot be kept
help: store a global value in this field, or remove `@ global` if the field should follow the containerA @ local parameter cannot come back in the result or in an error. A value that dies with this function cannot leave it either: not returned, broken out of a block that is returned, raised, or held by an error a call may raise. return! builds the result in the caller, needs a @ local result, and cannot use values that die with this function:
type Note = {
text: String;
}
let copy = fn(note: Note @ local) { Note { text = note.text; } };
let declared = fn(note: Note @ local) -> Note {
return! Note { text = note.text; };
};
let scratch = fn(note: Note) {
let draft: Note @ local = Note { text = note.text; };
return! draft;
};
let raise = fn(note: Note @ local) {
error :bad(Note { text = note.text; });
};
let labeled = fn(note: Note, flag: Bool) {
let draft: Note @ local = Note { text = note.text; };
let picked = 'found: {
if flag {
break 'found draft;
}
note
};
picked
};
let arm = fn(note: Note, flag: Bool) {
let draft: Note @ local = Note { text = note.text; };
match flag {
| True -> return draft
| False -> note
}
};
let fail = fn(note: Note) -> Int {
error :bad(note);
};
let propagated = fn(note: Note) -> Int {
let draft: Note @ local = Note { text = note.text; };
fail(draft)
};
$ship check returns/src/main.shp
error: `note` is `@ local`, so the result cannot contain it
--> returns/src/main.shp:5:37
|
5 | let copy = fn(note: Note @ local) { Note { text = note.text; } };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ returned here
help: `@ local` promises the result does not hold this parameter: leave it unmarked (`@ linked`) to return it, or write `@ local` on the result
error: `return!` builds this result in the caller, but the result is global
--> returns/src/main.shp:8:11
|
8 | return! Note { text = note.text; };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ built in the caller
help: write `@ local` on the result
error: `return!` cannot use a value allocated in this function
--> returns/src/main.shp:13:11
|
13 | return! draft;
| ^^^^^ allocated in this function
help: `return!` builds the result in the caller; use values that already belong to it
error: `note` is `@ local`, so an error leaving this function cannot hold it
--> returns/src/main.shp:17:9
|
17 | error :bad(Note { text = note.text; });
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ raised here
help: `@ local` promises nothing the caller sees holds this parameter: leave it unmarked (`@ linked`) to raise it
error: this value is local to this function and cannot be returned
--> returns/src/main.shp:28:3
|
28 | picked
| ^^^^^^ allocated in this function
help: return a value that already belongs to the caller, or build the result in the caller with `return!`
error: this value is local to this function and cannot be returned
--> returns/src/main.shp:34:22
|
34 | | True -> return draft
| ^^^^^ allocated in this function
help: return a value that already belongs to the caller, or build the result in the caller with `return!`
error: this call may raise an error that holds a value local to this function
--> returns/src/main.shp:45:3
|
45 | fail(draft)
| ^^^^^^^^^^^ may raise with a local value
help: pass values that already belong to the caller, or handle the error hereAn unannotated parameter the body keeps is inferred @ global, for functions and for let-bound closures, so passing a local through them is rejected at the call that passes it.
type Note = {
text: String;
}
type Kept = {
text: String @ global;
}
let keep_text = fn(text) { Kept { text = text; } };
let forward = fn(text) { keep_text(text) };
let leak = fn(note: Note @ local) { forward(note.text) };
let closure = fn(note: Note @ local) {
let keep = (text) =>
Kept { text = text; };
keep(note.text)
};
$ship check inference/src/main.shp
error: this argument is local and the parameter keeps it
--> inference/src/main.shp:11:45
|
11 | let leak = fn(note: Note @ local) { forward(note.text) };
| ^^^^^^^^^ local value cannot be kept
--> inference/src/main.shp:10:36
|
10 | let forward = fn(text) { keep_text(text) };
| ---- the parameter is kept here
help: pass a global value; this parameter is `@ global`
error: this argument is local and the parameter keeps it
--> inference/src/main.shp:16:8
|
16 | keep(note.text)
| ^^^^^^^^^ local value cannot be kept
--> inference/src/main.shp:15:19
|
15 | Kept { text = text; };
| ---- the parameter is kept here
help: pass a global value; this parameter is `@ global`A signature that callers were checked against before its body was, whether a let rec head, an annotation, or a head published for a forward reference, must write every parameter the body keeps:
type Kept = {
text: String @ global;
}
let rec swap = (first: String @ global, second: String, n: Int) -> Kept =>
if n == 0 {
| Kept { text = first; }
| swap(second, first, n - 1)
};
let keep: fn(text: String) -> Kept = fn(text) { Kept { text = text; } };
module Early {
let forward = (text: String) -> Kept => later(text);
}
let later = (text: String) -> Kept =>
Kept { text = text; };
$ship check signatures/src/main.shp
error: `swap` keeps `second` but its signature does not say so
--> signatures/src/main.shp:8:12
|
8 | | swap(second, first, n - 1)
| ^^^^^^ `second` is kept here
help: recursive and forward calls see only the written signature: write `@ global` on `second`
error: `keep` keeps `text` but its annotation does not say so
--> signatures/src/main.shp:11:63
|
11 | let keep: fn(text: String) -> Kept = fn(text) { Kept { text = text; } };
| ^^^^ `text` is kept here
help: callers see only the annotation: write `@ global` on `text` there
error: `later` keeps `text` but its signature does not say so
--> signatures/src/main.shp:18:17
|
18 | Kept { text = text; };
| ^^^^ `text` is kept here
help: recursive and forward calls see only the written signature: write `@ global` on `text`A function used as a value is known by its type. A callback slot that does not mark its parameter @ global rejects a function that keeps it, and a closure that keeps an inferred parameter may only be called directly:
type Note = {
text: String;
}
type Kept = {
text: String @ global;
}
type! Registry = {
read: fn(text: String) -> Kept;
}
type! Slot =
| Handler(fn(text: String) -> Kept)
| Empty
let apply = fn(text, f) { f(text) };
let keep = fn(text: String @ global) { Kept { text = text; } };
let remember = fn(text) { Kept { text = text; } };
let literal = fn(note: Note) {
apply(
note.text,
(text) =>
Kept { text = text; },
)
};
let named = fn(note: Note) { apply(note.text, remember) };
let bound = fn(note: Note) {
let hold = (text) =>
Kept { text = text; };
apply(note.text, hold)
};
let field = fn() { Registry { read = keep; } };
let payload = fn() {
Handler(
(text) =>
Kept { text = text; },
)
};
let returned = fn() {
(text) =>
Kept { text = text; }
};
$ship check callbacks/src/main.shp
error: this callback keeps `text`, but `apply` may pass it a local value
--> callbacks/src/main.shp:25:21
|
25 | Kept { text = text; },
| ^^^^ `text` is kept here
help: a callback may keep only parameters its slot marks `@ global`; that parameter is not `@ global` in `apply`'s callback type, so do not keep it
error: this callback keeps `text`, but `apply` may pass it a local value
--> callbacks/src/main.shp:19:41
|
19 | let remember = fn(text) { Kept { text = text; } };
| ^^^^ `text` is kept here
--> callbacks/src/main.shp:29:47
|
29 | let named = fn(note: Note) { apply(note.text, remember) };
| -------- passed here
help: a callback may keep only parameters its slot marks `@ global`; that parameter is not `@ global` in `apply`'s callback type, so do not keep it
error: this callback keeps `text`, but `apply` may pass it a local value
--> callbacks/src/main.shp:33:19
|
33 | Kept { text = text; };
| ^^^^ `text` is kept here
--> callbacks/src/main.shp:34:20
|
34 | apply(note.text, hold)
| ---- passed here
help: a callback may keep only parameters its slot marks `@ global`; that parameter is not `@ global` in `apply`'s callback type, so do not keep it
error: this callback keeps `text`, but calls through `read` may pass it a local value
--> callbacks/src/main.shp:37:38
|
37 | let field = fn() { Registry { read = keep; } };
| ^^^^ keeps `text`
help: a callback may keep only parameters its slot marks `@ global`; that parameter is not `@ global` in the type of `read`, so do not keep it
error: this callback keeps `text`, but calls through this payload may pass it a local value
--> callbacks/src/main.shp:42:21
|
42 | Kept { text = text; },
| ^^^^ `text` is kept here
help: a callback may keep only parameters its slot marks `@ global`; that parameter is not `@ global` in the payload's type, so do not keep it
error: this closure keeps `text` and is used as a value
--> callbacks/src/main.shp:48:19
|
48 | Kept { text = text; }
| ^^^^ `text` is kept here
help: write `@ global` on `text`; a closure used as a value tells its callers what it keeps only through its typeA store keeps its value as long as the container lives. Only a container built here, stored into directly, may hold a value that dies with its region; a @ local parameter, a @ local alias, or a top-level value may be global. A let mut binding must not outlive what it is assigned, and a callback that stores its argument into outer state keeps it:
type Note = {
text: String;
}
type Box = {
mut text: String;
}
let cache = Box { text = ""; };
let through_param = fn(box: Box @ local, note: Note @ local) {
box.text := note.text;
};
let through_alias = fn(note: Note @ local) {
let shared = Box { text = ""; };
let alias: Box @ local = shared;
alias.text := note.text;
};
let into_top_level = fn(note: Note @ local) {
cache.text := note.text;
};
let outlived = fn() {
let mut saved = Note { text = ""; };
let stash = fn() {
let note: Note @ local = Note { text = "a"; };
saved := note;
};
stash();
saved.text == "a"
};
let with_temp = fn(f: fn(text: String) -> Unit) {
let note: Note @ local = Note { text = "tmp"; };
f(note.text)
};
let from_callback = fn(note: Note @ local) {
let draft = Box { text = note.text; };
with_temp(
fn(text) {
draft.text := text;
},
);
draft.text == ""
};
$ship check stores/src/main.shp
error: `box.text` may outlive this value
--> stores/src/main.shp:12:15
|
12 | box.text := note.text;
| ^^^^^^^^^ local value cannot be kept
help: a stored value must live as long as the value that holds it; store a global value
error: `alias.text` may outlive this value
--> stores/src/main.shp:18:17
|
18 | alias.text := note.text;
| ^^^^^^^^^ local value cannot be kept
help: a stored value must live as long as the value that holds it; store a global value
error: `cache.text` may outlive this value
--> stores/src/main.shp:22:17
|
22 | cache.text := note.text;
| ^^^^^^^^^ local value cannot be kept
help: a stored value must live as long as the value that holds it; store a global value
error: `saved` may outlive this value
--> stores/src/main.shp:29:14
|
29 | saved := note;
| ^^^^ stored here
help: a stored value must live as long as the value that holds it; store a global value
error: this callback keeps `text`, but `with_temp` may pass it a local value
--> stores/src/main.shp:44:21
|
44 | draft.text := text;
| ^^^^ `text` is kept here
help: a callback may keep only parameters its slot marks `@ global`; that parameter is not `@ global` in `with_temp`'s callback type, so do not keep itA container at a mode variable takes only values that live as long as it does. A global array, an array that may be a global one, a field, and an element read back out may all outlive a local note. A parameter at 'm may go only into slots of 'm, a local closure's mutable 'm argument must be a value of 'm, and only builtins return a fresh value at a variable no parameter writes. A callback's parameter at the callee's 'm stands for the call's arguments of 'm, and an editor's contents reach the call's result:
type Note = {
text: String;
}
type Holder = {
items: Array(Note);
}
let saved: Array(Note) = Array.new();
let leak = fn(note: Note @ local) {
let mine: Note @ local = Note { text = note.text; };
Array.push(saved, mine);
};
let joined = fn(note: Note @ local, fresh: Bool) {
let items = if fresh {
| Array.new()
| saved
};
Array.push(items, note);
};
let projected = fn(note: Note @ local) {
let holder = Holder { items = saved; };
Array.push(holder.items, note);
};
let nested = fn(note: Note @ local) {
let outer: Array(Array(Note)) = Array.new();
Array.push(outer, saved);
Array.push(Array.get(outer, 0), note);
};
let stash = fn(note: Note @ 'm) {
Array.push(saved, note);
};
let fill = fn(note: Note @ 'm) {
let add = (item: Note @ 'm, into: Array(Note) @ 'm) =>
Array.push(into, item);
add(note, saved);
};
let fake_fresh = () -> Array(Note) @ 'm => saved;
let keep_items = fn(notes: Vector(Note) @ local) {
Vector.iter(notes, f = (note) => Array.push(saved, note));
};
let collect_local = fn(acc: Array(Note) @ 'm) {
let notes: Vector(Note) @ local = [Note { text = "a"; }];
Vector.iter(notes, f = (note) => Array.push(acc, note));
};
let edited = fn(note: Note @ local) -> Vector(Note) {
Vector.edit(
Vector.empty(),
f = (editor) => Vector.Editor.push(editor, note),
)
};
let callback_local = fn() -> Vector(Note) {
Vector.edit(
Vector.empty(),
f = fn(editor) {
let mine: Note @ local = Note { text = "x"; };
Vector.Editor.push(editor, mine);
},
)
};
$ship check mode-variables/src/main.shp
error: `saved` may outlive this value
--> mode-variables/src/main.shp:13:21
|
13 | Array.push(saved, mine);
| ^^^^ local value cannot be kept
help: a stored value must live as long as the value that holds it; store a global value
error: `items` may outlive this value
--> mode-variables/src/main.shp:21:21
|
21 | Array.push(items, note);
| ^^^^ local value cannot be kept
help: a stored value must live as long as the value that holds it; store a global value
error: `holder.items` may outlive this value
--> mode-variables/src/main.shp:26:28
|
26 | Array.push(holder.items, note);
| ^^^^ local value cannot be kept
help: a stored value must live as long as the value that holds it; store a global value
error: the container may outlive this value
--> mode-variables/src/main.shp:32:35
|
32 | Array.push(Array.get(outer, 0), note);
| ^^^^ local value cannot be kept
help: a stored value must live as long as the value that holds it; store a global value
error: `note` is `@ 'm`, so it cannot be kept
--> mode-variables/src/main.shp:36:21
|
36 | Array.push(saved, note);
| ^^^^ kept here
help: `'m` names the region the caller's arguments share: store `note` only into a slot of `'m`, or write `@ global` to keep it
error: this mutable argument must be a value of `'m`
--> mode-variables/src/main.shp:42:13
|
42 | add(note, saved);
| ^^^^^ may outlive `'m`
help: the callee stores `'m` values into it, so pass a parameter written `@ 'm`
error: the result is `@ 'm`, but no parameter is
--> mode-variables/src/main.shp:45:24
|
45 | let fake_fresh = () -> Array(Note) @ 'm => saved;
| ^^^^^^^^^^^^^^^^ only a builtin returns a fresh value at a region no parameter names
help: write `@ 'm` on the parameters the result comes from, or leave the result unmarked
error: this argument is local, and the callback keeps what it is passed
--> mode-variables/src/main.shp:48:15
|
48 | Vector.iter(notes, f = (note) => Array.push(saved, note));
| ^^^^^ local value cannot be kept
--> mode-variables/src/main.shp:48:54
|
48 | Vector.iter(notes, f = (note) => Array.push(saved, note));
| ---- the callback keeps it here
help: the callback's parameter shares this argument's mode variable, so keeping it keeps this argument; pass a global value, or do not keep it
error: `acc` may outlive this value
--> mode-variables/src/main.shp:53:15
|
53 | Vector.iter(notes, f = (note) => Array.push(acc, note));
| ^^^^^ local value cannot be kept
--> mode-variables/src/main.shp:53:52
|
53 | Vector.iter(notes, f = (note) => Array.push(acc, note));
| ---- the callback stores it here
help: the callback's parameter shares this argument's mode variable, so storing it stores this argument; pass a value that lives as long as where it is stored
error: `note` is `@ local`, so the result cannot contain it
--> mode-variables/src/main.shp:57:3
|
57 | Vector.edit(
| ^ returned here
help: `@ local` promises the result does not hold this parameter: leave it unmarked (`@ linked`) to return it, or write `@ local` on the result
error: `editor` may outlive this value
--> mode-variables/src/main.shp:68:34
|
68 | Vector.Editor.push(editor, mine);
| ^^^^ local value cannot be kept
help: a stored value must live as long as the value that holds it; store a global valueAn owned field takes only a fresh value, and an update must replace it. Storing into one takes what its record could: a parameter without 'm, or a record something global keeps, takes only global values. What is read back out of an owned array is as local as what was stored:
type Table = {
keys: Array(String) @ unique;
}
let saved: Array(String) = Array.new();
let tables: Array(Table) = Array.new();
let shared_init = fn(text: String @ local) {
let table = Table { keys = saved; };
Array.push(table.keys, text);
};
let aliased = (table: Table) =>
Table { keys = table.keys; };
let copied = (table: Table) =>
{ table with };
let unannotated = fn(table: Table, text: String @ local) {
Array.push(table.keys, text);
};
let kept_then_stored = fn(text: String @ local) {
let table = Table { keys = Array.new(); };
Array.push(tables, table);
Array.push(table.keys, text);
};
let read_back = fn(text: String @ local) {
let table = Table { keys = Array.new(); };
Array.push(table.keys, text);
Array.get(table.keys, 0)
};
$ship check ownership/src/main.shp
error: `keys` is `@ unique`, so it takes a fresh value
--> ownership/src/main.shp:9:30
|
9 | let table = Table { keys = saved; };
| ^^^^^ something else may already hold this value
help: build it here, for example with `Array.new()`
error: `keys` is `@ unique`, so it takes a fresh value
--> ownership/src/main.shp:14:18
|
14 | Table { keys = table.keys; };
| ^^^^^^^^^^ something else may already hold this value
help: build it here, for example with `Array.new()`
error: this update would share `keys`, which is `@ unique`
--> ownership/src/main.shp:17:3
|
17 | { table with };
| ^^^^^^^^^^^^^^ copied from the old record
help: give `keys` a fresh value in the update
error: `table.keys` may outlive this value
--> ownership/src/main.shp:20:26
|
20 | Array.push(table.keys, text);
| ^^^^ local value cannot be kept
help: a stored value must live as long as the value that holds it; store a global value
error: `table.keys` may outlive this value
--> ownership/src/main.shp:26:26
|
26 | Array.push(table.keys, text);
| ^^^^ local value cannot be kept
help: a stored value must live as long as the value that holds it; store a global value
error: `text` is `@ local`, so the result cannot contain it
--> ownership/src/main.shp:32:3
|
32 | Array.get(table.keys, 0)
| ^^^^^^^^^^^^^^^^^^^^^^^^ returned here
help: `@ local` promises the result does not hold this parameter: leave it unmarked (`@ linked`) to return it, or write `@ local` on the resultA behavior member and a contract implementation may keep no more than the type callers see, and may put parameters in one region only where that type writes one mode variable on them:
module behavior Collect(struct 'bag) {
add: pure fn!(bag: 'bag, name: String) -> Unit;
}
type Shelf = {
names: Array(String) @ unique;
}
let saved: Array(String) = Array.new();
module behavior Collect(Shelf) {
add = (shelf: Shelf, name: String) => Array.push(saved, name);
}
type Crate = {
names: Array(String) @ unique;
}
module behavior Collect(Crate) {
add = (crate: Crate @ 'm, name: String @ 'm) =>
Array.push(crate.names, name);
}
let main = fn() {
Bag.add(Bag.new(), "ada");
};
$ship check conformance/src/main.shp
error: implementation of `add` puts `bag` and `name` in one region `'m`, but the contract does not
--> conformance/src/Bag.shp:8:5
|
8 | let add = fn(bag: t @ 'm, name: String @ 'm) {
| ^^^ these share `'m` here
help: callers may otherwise pass values that die sooner than what holds them: write one mode variable like `@ 'm` on these parameters in the `.shc` signature
error: `Collect(Shelf).add` keeps `name` but its behavior operation does not say so
--> conformance/src/main.shp:12:59
|
12 | add = (shelf: Shelf, name: String) => Array.push(saved, name);
| ^^^^ `name` is kept here
help: generic code calls the member through the operation's type: write `@ global` on `name` there, or do not keep it
error: `Collect(Crate).add` puts `crate` and `name` in one region `'m`, but its behavior operation does not
--> conformance/src/main.shp:20:9
|
20 | add = (crate: Crate @ 'm, name: String @ 'm) =>
| ^ these share `'m` here
help: callers may otherwise pass values that die sooner than what holds them: write one mode variable like `@ 'm` on these parameters in the operation's typePipelines are calls, open-tag payloads are allocations, a constructor used as a value keeps what its pinned payloads keep, a pin anywhere on an update path pins the field, try arms see the arguments that raised, an error passes every enclosing frame, default arguments are captured, a spawned task has no caller to build a return! result in, and a spread reaches every parameter its fields fill:
type Note = {
text: String;
}
type Kept = {
text: String @ global;
}
type Slot =
| Pinned(String @ global)
| Empty
type Inner = {
x: String;
}
type Outer = {
inner: Inner @ global;
}
let keep_text = fn(text) { Kept { text = text; } };
let count = fn(text) {
let kept = Kept { text = text; };
1
};
let apply = fn(text, f) { f(text) };
let check = fn(text) -> Int {
error :bad(text);
};
let piped = fn(note: Note @ local) { note.text |> keep_text };
let tagged = fn(note: Note @ local) { :wrapped(note.text) };
let constructor = fn(note: Note) { apply(note.text, Pinned) };
let nested = fn(note: Note @ local) {
let outer = Outer { inner = Inner { x = ""; }; };
{ outer with inner.x = note.text; }
};
let caught = fn(note: Note @ local) {
try check(note.text) {
| :bad(text) -> count(text)
}
};
let raised = fn(note: Note @ local) {
let draft = Note { text = note.text; };
let raise = fn() {
error :bad(draft);
};
raise();
};
let defaulted = fn(note: Note @ local) {
let make = fn(~text = note.text) { Kept { text = text; } };
make().text == ""
};
let task = fn() {
spawn {
return! Note { text = "a"; };
}
};
let keep_label = fn(~text: String) { Kept { text = text; } };
let spread = fn(note: Note @ local) { keep_label(..note) };
$ship check flows/src/main.shp
error: this argument is local and the parameter keeps it
--> flows/src/main.shp:34:38
|
34 | let piped = fn(note: Note @ local) { note.text |> keep_text };
| ^^^^^^^^^ local value cannot be kept
--> flows/src/main.shp:21:42
|
21 | let keep_text = fn(text) { Kept { text = text; } };
| ---- the parameter is kept here
help: pass a global value; this parameter is `@ global`
error: `note` is `@ local`, so the result cannot contain it
--> flows/src/main.shp:35:39
|
35 | let tagged = fn(note: Note @ local) { :wrapped(note.text) };
| ^^^^^^^^^^^^^^^^^^^ returned here
help: `@ local` promises the result does not hold this parameter: leave it unmarked (`@ linked`) to return it, or write `@ local` on the result
error: this callback keeps its parameter, but `apply` may pass it a local value
--> flows/src/main.shp:36:53
|
36 | let constructor = fn(note: Note) { apply(note.text, Pinned) };
| ^^^^^^ keeps its parameter
help: a callback may keep only parameters its slot marks `@ global`; that parameter is not `@ global` in `apply`'s callback type, so do not keep it
error: `x` is global and this value is local
--> flows/src/main.shp:40:26
|
40 | { outer with inner.x = note.text; }
| ^^^^^^^^^ local value cannot be kept
help: store a global value in this field, or remove `@ global` if the field should follow the container
error: this argument is local and the parameter keeps it
--> flows/src/main.shp:45:27
|
45 | | :bad(text) -> count(text)
| ^^^^ local value cannot be kept
--> flows/src/main.shp:24:28
|
24 | let kept = Kept { text = text; };
| ---- the parameter is kept here
help: pass a global value; this parameter is `@ global`
error: `note` is `@ local`, so an error leaving this function cannot hold it
--> flows/src/main.shp:54:3
|
54 | raise();
| ^^^^^^^ raised here
help: `@ local` promises nothing the caller sees holds this parameter: leave it unmarked (`@ linked`) to raise it
error: `text` is global and this value is local
--> flows/src/main.shp:58:52
|
58 | let make = fn(~text = note.text) { Kept { text = text; } };
| ^^^^ local value cannot be kept
help: store a global value in this field, or remove `@ global` if the field should follow the container
error: `return!` cannot build a task's result in its caller
--> flows/src/main.shp:64:13
|
64 | return! Note { text = "a"; };
| ^^^^^^^^^^^^^^^^^^^^ a spawned task has no caller region
help: use `return`; a task's result must be global
error: this argument is local and the parameter keeps it
--> flows/src/main.shp:69:50
|
69 | let spread = fn(note: Note @ local) { keep_label(..note) };
| ^^^^^^ local value cannot be kept
--> flows/src/main.shp:68:52
|
68 | let keep_label = fn(~text: String) { Kept { text = text; } };
| ---- the parameter is kept here
help: pass a global value; this parameter is `@ global`Top-level values, which live for the whole program, and expect statements are checked too:
type Note = {
text: String;
}
type Kept = {
text: String @ global;
}
type! Registry = {
read: fn(text: String) -> Kept;
}
let registry = Registry {
read = (text) =>
Kept { text = text; };
};
let block = {
let note: Note @ local = Note { text = "a"; };
note
};
let keep_text = fn(text) { Kept { text = text; } };
test "Expect Keeps" {
let note: Note @ local = Note { text = "a"; };
expect keep_text(note.text).text == "a";
}
$ship check values/src/main.shp
error: this callback keeps `text`, but calls through `read` may pass it a local value
--> values/src/main.shp:15:19
|
15 | Kept { text = text; };
| ^^^^ `text` is kept here
help: a callback may keep only parameters its slot marks `@ global`; that parameter is not `@ global` in the type of `read`, so do not keep it
error: a top-level value is global and this value is local
--> values/src/main.shp:18:13
|
18 | let block = {
| ^ local value cannot be kept
help: a top-level value lives as long as the program; build it from global values
error: this argument is local and the parameter keeps it
--> values/src/main.shp:27:20
|
27 | expect keep_text(note.text).text == "a";
| ^^^^^^^^^ local value cannot be kept
--> values/src/main.shp:23:42
|
23 | let keep_text = fn(text) { Kept { text = text; } };
| ---- the parameter is kept here
help: pass a global value; this parameter is `@ global`Retention must be visible in the published contract. A contract leaves a parameter local unless it says @ global, whether the implementation declares the retention or the body keeps the value. Feed.Event { ... } agrees with Event in Feed, so only the retention is reported:
let main = fn() {
let event = Feed.Event { title = "shipped"; };
print(Feed.push(event));
};
$ship check hidden-retention/src/main.shp
error: implementation of `push` keeps `event` but the contract does not say so
--> hidden-retention/src/Feed.shp:5:5
|
5 | let push = (event: Event @ global) -> Int => 1;
| ^^^^ `event` is declared `@ global` here
help: what a function keeps must be visible in its published contract: write `@ global` on `event` in the `.shc` signatureRetention must be visible in the published contract. A contract leaves a parameter local unless it says @ global, whether the implementation declares the retention or the body keeps the value. Feed.Event { ... } agrees with Event in Feed, so only the retention is reported:
let main = fn() {
let event = Feed.Event { title = "shipped"; };
print(Feed.keep(event).title);
};
$ship check inferred-retention/src/main.shp
error: implementation of `keep` keeps `event` but the contract does not say so
--> inferred-retention/src/Feed.shp:10:18
|
10 | Kept { title = event.title; };
| ^^^^^^^^^^^ `event` is kept here
help: what a function keeps must be visible in its published contract: write `@ global` on `event` in the `.shc` signatureA local result must be visible in the contract too:
let main = fn() {
let note = Notes.Note { text = "ada"; };
print(Notes.copy(note).text);
};
$ship check local-result/src/main.shp
error: implementation of `copy` returns a local value but the contract does not say so
--> local-result/src/Notes.shp:5:5
|
5 | let copy = fn(note: Note @ local) {
| ^^^^ the result is `@ local`
help: write `@ local` on the result in the `.shc` signature, or return a global value