Dispatch a list message on the message in its head - #61
Merged
Conversation
['MOVE TO' player] -> vase now runs the vase's 'MOVE TO' method, with the rest of the list along as arguments, read with "head tail message". The send site needed nothing: OP_SEND already hands the recipient whatever value it was given, and a list rode through it intact -- only to fail messageConversion() in dispatch and land in the default method. So the rule lives in Object::dispatch, where pass and every send in the interpreter reach it too, rather than in the operator, where it would have had to be written three times. Nothing that dispatched before dispatches differently: head() is undefined for every value that is not a pair, so the new conversion is only ever tried where the old one already failed. A list whose head is no kind of message still falls to the default method, as it did. "message" stays bound to the whole list, so a method can read its arguments and still forward them with "message --> parent". No new tokens, no new enumerators, no change to the .acx layout -- the golden files come out byte for byte identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUmuc82NHf76PrRYjgLhq5
They sat at precedence 2, looser than every binary operator, so a prefix head swallowed whatever followed it: "head list = x" took the head of a comparison and quietly handed back UNDEFINED. The TODO left there asked what the right precedence was. It is 12, with length, numeric, string, random and unary minus -- the family they belong to -- so that "head list" is one operand and the operators around it see it that way. Nothing in intrptr.arch changes meaning. Its nineteen uses of head and tail either end an expression, where a prefix operator's precedence cannot matter, or were parenthesized already, which is the fossil of this bug: the parentheses had to be written because the precedence was wrong. Gorreven and Starship compile to byte-identical .acx files across the change, so no expression in either parses differently. "@" stays loose. It builds rather than selects, and an element wants to finish computing before it is joined on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUmuc82NHf76PrRYjgLhq5
Four sites wrapped "head x" only because head used to swallow whatever came after it. With head binding like a selector, the parentheses say nothing, and leaving them in would teach the next author to write them. Line 541 keeps its parentheses. They group a send inside a cons, which is a fact about "->" and "@" rather than about head, and a reader should not have to recall which of those two binds tighter. Gorreven and Starship still compile to the same bytes they did before the precedence change, so none of these lines parses differently than it did when the parentheses were there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUmuc82NHf76PrRYjgLhq5
"write" asks a value for its string, and a pair had none, so writing a list put out nothing whatsoever -- a misrouted list message looked like an empty string rather than like a list. Of the ways to be wrong that is the worst, since it leaves nothing behind to notice. The string form is the printed form: what the REPL echoes and what a message trace shows, so there is one rendering of a list and not two. Its consequences follow the house rule that a value converts if it sensibly can: "length [1 2 3]" is now 7, the width of the printed list, exactly as "length 100" is 3, and lists order lexicographically by that same text where before they did not order at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUmuc82NHf76PrRYjgLhq5
A string form for lists gave "length" and the orderings an answer they had no business giving. "length [1 2 3]" measured the printed width, 7, and "[1 2] < [3]" ordered two lists by the text they render as, which is an order in appearance only. Both are the cost of coercion reaching where it does not belong. So they ask instead. "length" of a list counts what is in it -- walked, O(N), since the spine is the only record of how long a list is -- and an improper tail counts as one of them, so "(1 @ 2)" is two. Ordering a list against anything yields UNDEFINED: there is no answer, which is not the same as the answer being no. Equality keeps asking what the list is made of, as it always did, and no longer accepts a list and its own printed text as equal. "&", "write" and "string" go on coercing, because text is what they were asking for in the first place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUmuc82NHf76PrRYjgLhq5
"within", "leftfrom" and "rightfrom" were cutting up the form a list prints as: "2" within [1 2 3] found the 2 at character four, and a list sliced into "[1 " and "2 3]". Answers to a question nobody asked. "within" is also where membership would want to live, if lists ever want a membership test, and an operator that already means something cannot quietly come to mean something else. UNDEFINED now keeps that door open; character four would have welded it shut. "&" is untouched, and still asks for the text it always wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUmuc82NHf76PrRYjgLhq5
The shipped protocol tells a thing about a move after the move; this one asks first, because the destination rides in the message. Nothing here includes the standard library -- the protocol is the whole program -- and nothing here touches the 'MOVE' protocol that shipped games subclass. The thing class has no last_location and needs none: when ['MOVE TO' dest] arrives, location is still the origin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUmuc82NHf76PrRYjgLhq5
A precondition that is ABSENT for everything without an objection covers nearly all of this, at no cost to the common case, and 'ADD SELF' wants no argument because the thing being added is the thing asking. What is left over is one question -- whether the destination will have it -- and the destination is the one thing sender cannot name, since sender is already saying which thing is moving. So the demo now spends exactly one list message, and one attribute to name its argument. The vise is the other half of the point: bolted down needs no argument at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUmuc82NHf76PrRYjgLhq5
Every value is now a list of at least one. A bare message and ['MOVE TO'] were already the same silence to a handler asking for arguments; now they are the same to everyone: dispatch always goes by the head, and the second case in Object::dispatch() is gone. A default method gets one spelling of the question, too. "case head message of" names what was sent whether or not it arrived in a list; before, each kind of message needed its own case, because the head of an atom was UNDEFINED. The tail stays undefined for an atom, on purpose. It is what tells an atom from a list of one, and it is what lets every loop that walks a list find the end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P458PFzYW18hjwnzjmTAJj
A list message to system is the staged dance in one send: the head first, then each element of the tail in order, with the reply of the whole being the reply of the last. So ['LOAD STATE' "file.acx"] -> system keeps its Boolean with no state machine on the sending side, and a control message rides in a tail like any other element, so a whole open-feed-close protocol fits in one send. Bare messages walk the same walk in one step -- an atom is the head of itself with an undefined tail -- so every staged protocol plays on unchanged. And a one-shot can never be caught between states by a save. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P458PFzYW18hjwnzjmTAJj
Every place that primed system and then sent the argument on the next line now asks the whole question at once: five 'WHICH OBJECT' dances, the 'PLAYER CMD' pair, both 'BANNER' pairs, and the save and load prompts, whose filename now rides in the message that wants it -- if ['LOAD STATE' read] -> system then What stays staged, stays for a reason. The vocabulary dances feed system from other objects' 'NAME' methods, where each send's sender is the word's owner, and the sorter is fed from a loop; both are genuinely many sends, which is what the staged form is for. The goldens moved by ten bytes of statements and not one line of Turtle: the world the games compile to is identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P458PFzYW18hjwnzjmTAJj
VersionString has moved exactly once before now: it was born saying 3.0 when the C++ interpreter was, and the number has only ever named eras. This branch earns the next one twice over. By the letter: head 5, length of a list, and orderings on lists all answer differently than they did, and changed answers to old questions are a major no matter how accidental the old answers were. By the spirit: a game compiled against this library requires this interpreter, and "a 4.0 game needs a 4.0 interpreter" is the sentence a major version exists to say. The .acx format version stays at 1, deliberately: the byte layout did not move, and that number describes the layout, not the language. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P458PFzYW18hjwnzjmTAJj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A message can now carry arguments:
The head names the method; the whole list stays bound to
message, so thereceiver reads its arguments with
tail messageand forwards them upwardwith a bare
message --> parent.The feature is one rule
Dispatch always goes by the head. A list supplies its own, and — since
head <atom>now yields the atom — every other value is the head ofitself. Each half of the rule is a few lines:
Object::dispatch()asks the sent value's head for a message, with nofallback and no second case.
['MOVE TO'] -> vaseand'MOVE TO' -> vaseare the same send, and
['SHATTER' 3] -> vasereaches the default methodexactly as an unclaimed atom always did.
IValue::head()returns the value itself, so every value reads as a listof at least one. The tail stays undefined for an atom, on purpose: the
tail is what tells an atom from a list of one, and what lets every loop
that walks a list find the end.
A default method gets one spelling of the question, too:
head messagenames what was sent whether or not it arrived in a list. Before the head
rule,
case message ofmatched bare messages and never a list's head,while
case head message ofmatched list heads and never a bare message —a fork on exactly the distinction the feature set out to blur.
messagedeliberately stays the whole list rather than being rebound tothe head — that is what makes forwarding free.
systemspeaks the same ruleA list message to
systemis the staged dance in one send: the headfirst, then each element of the tail in order, with the reply of the whole
being the reply of the last. So
keeps its Boolean with no state machine on the sending side; a control
message rides in a tail like any other element, so
['INIT SORTER' "b" "a" 'CLOSE SORTER']is a whole open-feed-closeprotocol in one send; and a one-shot can never be caught between states by
a save. The implementation is a walk:
executeDefaultMethodfeeds eachelement to the old single-step logic, extracted unchanged as
interpret_().figureState_and the save format are untouched, and abare message is the one-element walk — an atom is the head of itself with
an undefined tail — so every staged protocol plays on exactly as before.
And
intrptr.archnow speaks it. Every place that primedsystemandthen sent the argument on the next line asks the whole question at once —
five
'WHICH OBJECT'dances, the'PLAYER CMD'pair, both'BANNER'pairs, and the save and load prompts, whose filename rides in the message
that wants it:
if ['LOAD STATE' read] -> system. What stays staged,stays for a reason: the vocabulary dances feed
systemfrom otherobjects'
'NAME'methods, where each send's sender is the word's owner,and the sorter is fed from a loop — genuinely many sends, which is what
the staged form is for.
Four things the feature turned up on the way
headandtailbound looser than every binary operator (precedence 2).A prefix operator's precedence governs only what it swallows to its right,
so
head tail message = UNDEFINEDparsed ashead tail (message = UNDEFINED)and quietly asked whether the whole comparison had a head. They now bind at
12 with the other unary selectors —
random,length,numeric,string—which is where they always belonged. The parentheses
intrptr.archwascarrying only to work around this are gone, except at line 541 where they
still separate a send from its operand and earn their keep.
Oracle for that change: Gorreven and Starship compile to byte-identical
.acxacross it, which proves all nineteenhead/tailsites in thelibrary parse the same as before.
writerendered any list as nothing at all — the one way of being wrongthat leaves nothing behind to notice it by.
PairValue::stringConversionnow returns the display form, so
write,&, and a debug trace all show alist the same way.
Operators that care about structure now ask about it rather than
inheriting whatever the new string form implied:
length [1 2 3]length (1 @ 2)[1 2] < [1 3][1 2 3] = "[1 2 3]"[1 2 3] = [1 2 3]2 within [1 2 3]2 leftfrom [1 2 3]"[1 ""items " & [1 2 3]"items [1 2 3]"The rule: coercion answers "how do you read as text?" A test is needed only
when an operator's meaning depends on what the value is made of.
lengthcounts, orderings can't order, and text surgery has no business inside a
list —
withinin particular stays free for a membership meaning later.=and~=were already structural (isSameValueAsruns before anyconversion); all that changed is that a list stopped comparing equal to its
own printed form by accident.
lengthwalks the spine, the waystd::list::sizeonce did. An impropertail counts:
(1 @ 2)holds two things.length []is UNDEFINED, not 0, and stays that way for now. An emptylist terminates as
UndefinedValue, which is also what an attribute nobodyever set reads as — so answering 0 would claim that a missing attribute
contains zero things. Common Lisp gets
(length nil)= 0 because itsnilwears two hats, empty-list and false, both of them values; Archetype's
UNDEFINED wears a third, missing. The loss is narrow:
while lst doandlst = UNDEFINEDare the idioms, and neither needs a count. If a distinctempty-list value is ever worth its own type, it must be false in a
conditional, or
while lst dowould spin.The demo: a movement protocol that asks first
demos/moving.archis the feature's honest showcase, and most of what itshows is arguments being unnecessary. The shipped protocol in
intrptr.archmoves a thing by mutating first and telling it afterward, sorefusal means undoing — which is what
last_locationexists for. Askingfirst fixes nearly all of that with no arguments at all: a precondition is
a method that is ABSENT for everything without an objection, and
senderalready names the thing doing the asking. One thing is left over — a
precondition about the destination has to be handed the destination, and
senderis spoken for — and that is the file's only list message, costingone attribute (
dest_) to give the argument a name. Theannouncedclassshows forwarding:
message --> thing, whole list, nothing unpacked andnothing rebuilt. The shipped
'MOVE'protocol is subclassed by the gamesand is not going anywhere.
Testing
testListMessages_in TestObject.cc covers dispatch with an argument; theidentity of
['MSG']with a plain message; an unclaimed head, anunconvertible head, and a bare unclaimed message all reaching the default
method, where
head messagenames each of them; the reply value; and-->forwarding with arguments intact.
testListLiterals_covers the operatortable above and the head rule itself:
head 5is 5,head [5] = head 5,and
tail 5andhead UNDEFINEDare UNDEFINED.testListMessages_inTestSystemObject.cc covers the one-shot sorter with its last-echo reply,
['NEXT SORTED']as the bare message, a control message riding in a tail,and one-shot
['WHICH OBJECT' "grab"]with no priming and no second send.One-shot
['SAVE STATE' f]and['LOAD STATE' f]verified TRUEend-to-end in the REPL.
18/18 suites. Both golden checks pass, regenerated for the intrptr
conversion: the Turtle — the reviewable half, the world state — did not
move by a line; only the statement bytes did. Gorreven was play-tested
through every converted path, including a save/load round trip and the
pronoun dance, with output identical to main's.
Compatibility
No new tokens and no new enumerators, so
.acxfiles and golden binariesare untouched — but that cuts the wrong way for forward compatibility: an
old interpreter finds nothing to fail on. It silently routes a list message
to the default method, and answers UNDEFINED for the head of an atom. And
since
intrptr.architself now sends list messages, every game compiledfrom this branch requires this interpreter — ship it first.
The version number now says so: this branch is Archetype 4.0, the
first bump since the version string was born saying 3.0 alongside the
C++ interpreter. It earns the major twice over — by the letter, because
head 5,lengthof a list, and orderings on lists all answerdifferently than they did, and changed answers to old questions are a
major no matter how accidental the old answers were; and in spirit,
because "a 4.0 game needs a 4.0 interpreter" is the sentence a major
version exists to say. The
.acxformat version stays at 1: the bytelayout did not move, and that number describes the layout, not the
language.
🤖 Generated with Claude Code
https://claude.ai/code/session_01P458PFzYW18hjwnzjmTAJj