Dispatch a list message on the message in its head - #61
Open
gitosaurus wants to merge 8 commits into
Open
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
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 fallback
Object::dispatch()asks the sent value for a message. If it hasn't gotone, it now asks the value's head. Every value that is not a pair has an
undefined head, so nothing that dispatched before dispatches differently:
['SHATTER' 3] -> vasereaches the default method exactly as it did whenthe head named nothing.
['MOVE TO'] -> vaseand'MOVE TO' -> vaseare the same send. There isno separate argumentless case to write.
messagedeliberately stays the whole list rather than being rebound tothe head — that is what makes forwarding free.
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.Testing
testListMessages_in TestObject.cc covers dispatch with an argument, theidentity of
['MSG']with a plain message, an unclaimed head and anunconvertible head both reaching the default method, the reply value, and
-->forwarding with arguments intact.testListLiterals_covers theoperator table above;
testPairs_covers the string form.18/18 suites. Both golden checks pass, and every compiled game is
byte-identical to main.
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 and silently routes a list message
to the default method. Ship the interpreter before any game that uses one.
🤖 Generated with Claude Code
https://claude.ai/code/session_01AUmuc82NHf76PrRYjgLhq5