Skip to content

Expression cards in the Logic Programmer, editable as text - #1766

Open
rubensworks wants to merge 52 commits into
master-1.21-ltsfrom
feature/expression-cards
Open

rubensworks wants to merge 52 commits into
master-1.21-ltsfrom
feature/expression-cards

Conversation

@rubensworks

@rubensworks rubensworks commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

One variable card can now hold a whole expression built in the existing Logic Programmer, for example

item => item.nbt().size() > 3
l => reduce((+), map((stringLength), l), 0)
(a, b, c) => a > b ? a * c : b - c

instead of 6 to 8 cards in a Variable Store glued together with operator, apply, pipe and flip. The expression can be built with slots, or typed and copied as text, in the same view. The text is close to the JavaScript of Integrated Scripting, so simple functions can be pasted from scripts. Value types and operators of add-ons, such as the chemicals of Integrated Mekanism, work the same as those of Integrated Dynamics.

This PR now also contains what were the stacked PRs #1767 (text) and #1768 (editing in place), which are closed.

Screenshots of the current Logic Programmer: https://claude.ai/artifact/Fwyz9rB9VqPMmZY6FYasuF

What was built

Data model (api/evaluate/expression/tree, core/evaluate/expression/tree)

  • Expression(root, parameters) with sealed ExpressionNode: Operator(op, children), Ref(variableId), Const(value), Param(index), plus Empty for slots that are still being edited.
  • Serialized as nested NBT (not nested strings). Deserialization rejects trees deeper than 64 levels, more than 8 parameters, unknown operators/types and out-of-range parameter indexes. A 60-level tree is about 3.5 KB of binary NBT (unit test).
  • The card also stores the known output types of the cards it references (referenceTypes). The LP uses them when the card is loaded again, and the card's tooltip uses them for its type, so it matches what the LP showed. Networks validate against the type inferred without them, as they check the actual variables.
  • It also stores the text the expression was typed as (text), if any, which only the LP uses (see Text).

Evaluation

  • Without parameters, ExpressionVariableFacade.getVariable builds nested LazyExpressions. The root is cached in the network under the card id (same as OperatorVariableFacade, so LazyExpression-based advancement predicates still see it). Inner nodes use a cache local to the tree instead of derived ids, so they can never collide with other variable ids. Invalidation propagates through the normal listener chain; lastNetworkHash handling mirrors OperatorVariableFacade.
  • With parameters, the card's value is a ValueOperator wrapping the new ExpressionOperator (extends OperatorBase). Inputs are the parameter types, output is inferred. Referenced cards are bound as variables when the card is built, like CurriedOperator does, and a wrapper variable forwards their invalidation. Each call evaluates nodes lazily and at most once, without registering listeners, so choice only evaluates the branch it needs.
  • materialize() replaces refs by their current materialized values and keeps parameters. Materializing a pipe that contains an expression operator also materializes it (reuses the Fix crash when displaying piped operators #1705 pattern in CombinedOperator). ExpressionOperator.Serializer writes refs as their current values, like the curry serializer.

Compatibility rule: a single operator whose inputs are all cards and that has no parameters is written as a plain OperatorVariableFacade, exactly as before. Only trees that need the new features produce the new integrateddynamics:expression facade. Old cards load and evaluate unchanged; loading an old card into the LP yields a one-level tree.

Type inference (ExpressionTypeChecker): walks the tree with getInputTypes, validateTypes and getConditionalOutputType, returning structured errors (node path, slot, expected, actual, message) and the number of empty slots.

Names (ExpressionPrinter, OperatorNames): operators are named the way Integrated Scripting names them. A call uses the operator's scoped interact name (length, nbt, getInteger), which IS uses for methods on values, when the type of its first argument tells it apart from other operators with that name; otherwise it uses the global interact name (stringLength), which IS uses for idContext.ops. 248 of the 267 scoped names belong to one operator, and the other 19 are always told apart by a concrete first argument type, so the fallback mostly shows up for empty slots, untyped parameters and operator values ((stringLength)). Operators of add-ons are named the same way, from the same registry: amount of Integrated Mekanism's chemicals and of fluids is told apart by the type of its input. A call with a scoped name is written as a method of its first argument (item.size()), as IS calls the methods of values. Calls with a global name (stringLength(x)), operators that take an operator first (map(f, l)), incomplete choices and calls on an empty slot or a number literal (where 1.max(2) would read as a decimal number) stay calls. Only + - * / % == != < <= > >= && || are infix and ! prefix, each meaning exactly one operator, except that + with a string input is concat (see Text), and complete choices print as c ? a : b, so names never contain LP symbols such as NBT{}.get_integer or =NBT=. OperatorNames.resolve(name, firstArgumentType) is the inverse, and a unit test checks for every registered operator that its printed name is an identifier that resolves back to it. Used in the card tooltip (Expression: and Type: lines) and in the LP, where the text, chips, breadcrumb tabs and status messages use the same names.

Logic Programmer (upgraded OperatorLPElement, no new element)

  • A slot accepts a card (ref), a typed value, a real item, a parameter, a nested operator, or an operator as a value. A real item is an item constant in slots that accept items. In other slots, it gives the value that the element of the slot's type reads from it, such as a fluid from a bucket, a block from a block item or, with Integrated Mekanism, a chemical from a chemical tank. Such a constant is shown as that item. Putting the same item back keeps the constant, so a typed 250 mB stays 250 mB although its bucket reads as 1000 mB, and when the item does not show the value exactly, its tooltip adds the value (Lava (250 mB)).
  • Left-click a slot to select it. A value field appears for text-editable types (Boolean, Integer, Long, Double, String, NBT, Number, Any); Enter writes the constant. While a slot is selected, the operator list only shows operators whose output fits the slot. Picking one puts it in the slot and opens it, so its inputs can be filled in next. Tabs at the top of the config area lead back up; clicking a nested chip twice reopens it. Tabs in the middle that do not fit, as for x.fluidStack().block().breakSound(), are collapsed into one … tab that leads to the last operator it stands for and lists them in its tooltip, and names that still do not fit are cut off. Right-click clears a slot; Escape or a click on the empty config area cancels a selection.
  • The operator list shows the operator that is open in the slots as selected, as it does for the operator of a loaded card. Clicking that entry does nothing, as activating it would replace the whole expression.
  • In an Operator slot (the first input of map, filter, reduce, apply, ...) every operator is listed. Picking one that cannot give an operator puts it in as a value, so map((stringLength), l) needs no extra card. Operators that may give an operator (flip, pipe, apply) are nested as calls, since that is what you usually want there; shift-click puts those in as a value too.
  • Chips show the operator's short name (size, nbt, >) or the value at the largest scale that fits, with a badge while a nested operator has empty inputs. A chip is outlined red when it or anything inside it has a type error, and hovering it highlights its part of the expression.
  • A side panel shows the expression. It is shown and hidden with an arrow button at the top right, like the vanilla recipe book, and is hidden by default, so the Logic Programmer keeps its old 256px width until it is opened. Expressions remain editable in the slots without it. The + of the List element was in that corner, under the arrow button, so it moved left, next to the List element's type.
    • Right of its title: Undo, Redo, Format and Copy (see Text).
    • Below the title: the parameters as chips, followed by a + that reads + Parameter while there are none. While typed text is not used yet, it adds the parameter to the text instead, as the text would otherwise undo it. Drag a chip onto a slot to use it there, or click it and then the slot; while selected, type to rename it, and set its type with the arrow buttons next to the name, the same selector (WidgetArrowedListField) the List element uses for its type. It lists the types of Integrated Dynamics, then the registered types of add-ons by name, such as Chemical. The name and the type fill the row of the config area, both with the gray background of that selector. Right-click a chip to remove it. Chips show only the name, with the name and type in their tooltip (the type in its color) and in the type of the expression, and wrap onto as many rows as needed at most half the panel wide each, so all 8 parameters always show.
    • Below that, a dark box that fills the rest of the panel with the expression as text: it wraps, shows the edited node in yellow, errors in red and empty slots as a gray ?, highlights the part of a hovered slot, and can be clicked and edited (see Text).
    • The footer of that box shows the inferred type in the colors of the value types (as in card tooltips), or the first type error instead: 2 type error(s), the first in input 2 of *, or the message of an error of an operator as a whole (such as choice branches of different types), since that has no slot to hover. Hovering an error shows its full message. Whether the expression can be written is shown by the existing check mark next to the operator name.
    • While no operator is selected, the panel looks like that of an expression without parameters, with the same buttons and the dark box in the same place, with Type an expression, such as (x: integer) => x * 2, or pick an operator as placeholder, so an expression can be typed right away (see Text) and nothing moves once it is used. As there is no expression yet, the buttons work on the text: Undo reverts it, Redo has nothing to redo, and + Parameter adds a parameter to the text, as x => in front of it or after its other parameters. The existing "Bind new Variable Card" arrow already points at the operator list.
    • All text in the panel is drawn at full size, since text scaled to 75% gets uneven strokes at GUI scales 2 and 3.
  • Parameter types default to Any and take the type of the first slot they're dropped in that is not Any (a concrete type, also of an add-on, or Number).
  • Loading a card back into the LP shows the actual referenced cards when they are in the player's inventory. Otherwise the slot gets a placeholder card with the referenced id and its stored type ("Reference to the variable card with this ID"), instead of the fake value cards with default values that were used before. Cards placed in slots are remembered while navigating through nested operators.
  • Slot edits are applied identically on both sides through one LogicProgrammerExpressionActionPacket carrying small actions (const, op, param, clear, nav, parameter add/remove/rename/type), handled by the side-independent ExpressionEditor. The server only accepts slot edits for the node being edited and ignores malformed actions.
  • Labeller and write slot are unchanged.

Text (ExpressionParser, ExpressionPrinter.printExact)

  • Clicking the expression places the caret and opens the operator under it in the slots, so text and slots follow each other.
  • Typed text is applied once it parses and typing pauses for 750 ms, or right away with Enter, so the card is rewritten without confirming. Until then, the text around what was typed keeps its colors, and the footer shows the type of the typed text, or the parse error with its line and column (Line 1, column 24: The text ends too early, a value is missing), which is also underlined in the text. Applied text stays as typed (see below). Shift+Enter adds a line. Clicking outside the text, hiding the panel or closing the gui applies valid text first. While the text does not parse, slots, parameters and tabs ignore clicks, so the text and the slots cannot both change the expression.
  • Escape stops typing. Valid text is applied right away, as with Enter, and text that does not parse is reverted. Before, Escape reverted text that was not applied yet, so typing + 3 and pressing Escape within 750 ms lost it, while pressing it a moment later kept it.
  • Typing without an operator. While no operator is selected, the panel takes typed text too, with the same parsing, footer and applying. Once applied, the element of its root operator becomes active, as for another root operator below, and typing continues in the panel of that expression, with the text kept as typed and focused.
  • Undo, Redo, Format and Copy are 12 px icon buttons right of the panel title, also while typing. The font has no symbols for these, so the icons are drawn pixel by pixel, in the colors and with the shadow of the text of text buttons. Undo and Redo go back and forth through up to 64 changes to the expression, from slots, parameters and text alike, also across a change of root operator, and are reset when another element or card is opened. Undo also reverts typed text that is not applied yet. Ctrl+Z and Ctrl+Y (or Ctrl+Shift+Z) work too. Format writes the expression in its usual form (see below). Copy puts the text on the clipboard and shows a check for a moment.
  • Typed text is kept as typed. (x: integer) => x.add(3) stays x.add(3), also with its spaces, brackets and line breaks, and is not printed as x + 3. The editor keeps the text next to the tree, as do the history and the written card (text next to expression), so it comes back with Undo and Redo and when the card is loaded. The text is only shown while it still reads as the same tree, and otherwise the tree is printed, such as after a label that it uses changed. An edit of a slot or parameter that changes the tree prints it from then on, and Undo brings the text back. Text that is the usual form anyway is not stored. Typing the same tree in another way is applied as a change, so it is one Undo step. ExpressionParser.tokenize records where the parser read each node, so each character of typed text belongs to its innermost node, as for printed text: brackets belong to the value in them, and spaces to the operator around them. So colors, the highlight of a hovered slot and opening the node under the cursor work the same. The Format button, between Redo and Copy, writes the usual form instead, applying typed text first if it parses. It is disabled while there is nothing to format. In the panel without an operator, it replaces the typed text by its usual form, which is then applied as typed text. The card tooltip keeps its short printed form, as it shortens values. Plain operators are still written as classic cards, without the text (see Things to decide on).
  • Selecting and moving, taken from the editor of Integrated Scripting's terminal (Fix scripting terminal editor bugs (scroll, cursor, focus, sync) IntegratedScripting#77 and Add code editing features to the scripting terminal IntegratedScripting#86). Double-clicking selects a word, and a third click the whole text. Words end at names, numbers, symbols and brackets, so item.isStackable() has item, ., isStackable, ( and ), where vanilla's text fields only stop at spaces. Ctrl with Left, Right, Backspace or Delete moves or deletes by such words. On macOS that is Option, and Cmd goes to the start or end of the line, or with Up and Down of the text, as in its own text fields, where vanilla treats Cmd as Ctrl. The bracket next to the cursor and its match get a background, skipping brackets in strings.
  • Completing methods, with the keys of Scripting terminal: idContext.ops completion and signature hints IntegratedScripting#96. While typing a name after a point, as in item.si, a list shows the operators that can be called on the value before the point, with their other inputs and output in the colors of their types, as in strength (Block): Double. The type of that value comes from the type checker, so this works after parameters, calls, literals, references and brackets. Up and Down choose one, and Tab, Enter or a click inserts it with its brackets, closed when it takes no other inputs. Escape, moving the cursor or clicking elsewhere closes the list. While the cursor is inside the brackets of a call, a popup shows the call in the colors of its types, with the input at the cursor underlined, as in Item.strength(Block): Double. As in Integrated Scripting's editor, it is a single line above the line of the cursor, starting at the left of the text, below the first line as there is no room above it, moved left to stay on the screen, and not shown while the list is. The footer always shows the type or errors of the expression. Both come from parsing the text up to the cursor (ExpressionParser.complete). The methods of a type (OperatorNames.getMethods) are the operators whose first input takes it, named as the printer names them, so each listed name is read back as its operator. The list is drawn after the rest of the gui, as slots and labels are drawn without depth and would cover it. The same list shows types after the colon of a parameter, as in (x: int, and after @, as in @ite, with the name the game gives them next to theirs, in the color of the type (item Item), and also when typing the name of an add-on's type without its namespace (chem for integratedmekanism:chemicalstack). Parameters only list the types they can have, and values no categories.
  • Typing never waits on the server. Parsing, name resolution and type checking all run on the client, against the ref types the editor knows plus the cards in the player's inventory. Nothing is sent until the text is applied. Then the client applies the result locally and sends the tree once, as a set action in the same packet. The server deserializes it with the existing limits (depth 64, 8 parameters, known operators and types) and also checks ExpressionEditor.isEditable: the root is an operator, at most 1024 nodes, parameter names that the text form can read back, parameter types that are registered and not a category (other than Any and Number), and only operators that have a slot for each input. Anything else is ignored. The client checks the node limit while parsing (The expression has more than 1024 parts), so such text is never applied only to be ignored.
  • Another root operator switches the element. Each LP element is one root operator, so if the text has another root (typing (a + b) * 2 while in >), the element of that operator becomes active, the same way loading a card does. The known ref types and the actual cards placed in slots are carried over first, since deactivating the old element forgets them. Otherwise, applying keeps the open operator in the slots if it still exists. The set action does the same while no operator element is active, which is what the empty panel uses.
  • Format. It is close to the JavaScript of Integrated Scripting, so simple functions from scripts can be pasted, with at most their parameter types added, as in (item: item) => item.isStackable() && item.size() >= 16 from the IS book. Only syntax that means the same in both is taken over (see Known limitations).
    • Calls use the scoped name when the type of the first argument tells it apart (length(s) for strings, length(l) for lists), and otherwise the global one (stringLength). Both are accepted when parsing.
    • As for methods in scripts, item.size() can be written for size(item), with further arguments in the parentheses (s.concat(t)), chained, and on any value ("abc".length(), #5.length()). It binds tighter than ! and infix operators, so a receiver with symbols is printed in brackets, as in (a + b).length(). Calls with a scoped name are printed this way (see Names), so the usual form keeps methods, and Format turns size(item) into item.size(). The names and argument order are ID's, so list.filter(f) is a type error, as filter takes the operator first, unlike JavaScript's array method.
    • Infix + - * / % == != < <= > >= && || with C precedence, prefix !, and c ? a : b. === and !== are read as == and !=, which already compare strictly.
    • + is concat as soon as one of its inputs is a string, both when parsing and when printing. concat of inputs without a known type is printed as stringConcat(a, b), as + would be read as addition, and (+) is addition.
    • ? is an empty slot, and (name) is an operator as a value, such as (+) or (stringLength).
    • Parameters are declared in front: x => ..., or (x: integer, y) => ... with optional types. -> is read as well. Types are written by their id, without the namespace for those of ID, where items and fluids are item and fluid, as the game calls them (itemstack and fluidstack are read too). The global names of their operators, such as itemstackSize, keep the ids, as scripts use them, and TODOs in ValueObjectTypeItemStack and ValueObjectTypeFluidStack mark them to be renamed in the next major. Types of add-ons are written with their namespace, as in (c: integratedmekanism:chemicalstack), which can be left out when no other type has that name ((c: chemicalstack)). A parameter that holds an operator can be called: f(x) is apply(f, x), f() is apply0(f), up to apply3 for three inputs, and it is printed with apply.
    • Literals are true/false, 1, 5L, 1.5/1e3/2d/NaN/Infinity, and strings in double or single quotes with \" \' \\ \n \t \r \uXXXX escapes, printed in double quotes.
    • Lists are written as ["apple", "pear"], [1, 2] or [[1], [2, 3]], with the type their values share, or Any for [1, "a"] and []. Their elements must be values, and a comma after the last one is allowed. A list whose type its values do not give, such as an empty list of Strings, keeps the @list{...} form below.
    • NBT is written as the tag itself, as in commands: @nbt{a:1b}, @nbt [1,2], @nbt 5, and @nbt() for no NBT.
    • Any other value is written as its type with its serialized form, as in @item{count:1,id:"minecraft:diamond"}, @block{Name:"minecraft:oak_log",Properties:{axis:"x"}} or @fluid{amount:1000,id:"minecraft:water"}, so the printer is exact for every value, also for value types of add-ons, such as @integratedmekanism:chemicalstack{amount:1000L,id:"mekanism:hydrogen"} (or @chemicalstack{...} when typed). An item without a count is a single one, and ids can leave out the minecraft namespace. The part after the type is the same as what IS gives for item.id_item, as both use the value type's serializer.
    • Typed values are read back and compared with what was typed. A value that would silently become something else, such as an unknown item (which used to turn into an empty slot), a malformed list (an empty list), @nbt{a:1b} in the old internal form, or a bad entity UUID, is an error that shows what it would be read as: Not a valid item value, it would be read as {}. Defaults that deserialization adds are allowed, and so are numbers of another type with the same value, also in lists and arrays, such as amount: 1000 for an amount that is stored as 1000L.
    • #42 refers to a card by ID. #limit, #"my label" or #'my label' refers to it by label, and only resolves when exactly one card has that label. Copy prints #label only in that case, and #id otherwise.
  • Pasted text. Other kinds of spaces, such as non-breaking ones from web pages, are whitespace, and curly quotes from word processors, double and single, work as quotes. Strings escape the section sign, control characters and curly quotes, as Minecraft's text fields drop the former when pasting and the latter end a string. Text over 8192 characters gives an error instead of being cut off at that length.
  • ExpressionPrinter.printExact is the inverse of ExpressionParser.parse, and the tests check this both ways.
  • Implementation:
    • ExpressionParser is a recursive descent parser with a small lexer, for at most 8192 characters, with 31 translatable error messages in ExpressionParseException. ExpressionEditor.parse adds the LP's node limit. The parser infers the type of each node once while building it (ExpressionTypeChecker.Inference), as + needs the types of both inputs and calls the type of their first, so a long chain such as a + b + c + ... stays linear.
    • ExpressionTextField (client only) uses vanilla MultilineTextField as the text model, and draws it with a color and background per character, a selection, a blinking caret, an underline for parse errors and a scrollbar, since vanilla's edit boxes can only draw a single color. It is kept in the container, so it survives the panel being recreated when navigating or when the element changes. It tells printed text, applied text that is still as typed, and text that is not applied yet apart. Words, brackets and the keys that differ from vanilla are in ExpressionTextNavigation, which has no client code, so it is unit tested. A click places the cursor at the nearest boundary between characters, where vanilla's text model takes the one before the character under the mouse, which made it hard to click between two brackets.
    • ExpressionStartPanel (client only) is the panel without an operator. It is not a sub-GUI, since the screen uses an empty sub-GUI list to show its "Bind new" and "Modify" hints, so the screen forwards input to it. It shares the layout, footer, text field and buttons (ExpressionPanelButtons) with OperatorLPElementRenderPattern.
    • Applying is done from the screen's tick, after the sub guis tick, and before the container closes, as it can replace the panel.
    • ExpressionPrinter.tokenizeExact gives the text as tokens with the path of their node, which maps each character to its node for colors and for navigating from the caret. ExpressionParser.tokenize gives the same for typed text. The parser records a span for each node it reads, children before their parents, and widens the span of a value in brackets to its brackets. As the empty node is a single instance, those are matched to their paths in the order they were read.
    • ExpressionHistory records changes by observing the expression and its text while rendering, so no action needs to know about it.
    • ContainerScreenLogicProgrammerBase forwards scrolling, dragging and releasing to the panel, since CyclopsCore sub-GUIs only get clicks and keys, adds the value of an inexact item constant to the item's tooltip, and draws the list of methods and the signature popup last, hiding the tooltips of slots below them.
    • Renaming a parameter now also rejects names the text could not read back, such as true, NaN or 1x.

Book: new "Expressions" page after "Logic Programming" in the manual, covering the panel and how to open it, slots (including items that hold a value, such as buckets), nesting, operator values, parameters and editing as text, also without a selected operator, with how close the syntax is to Integrated Scripting, how values are written, also of add-ons, the keys for selecting and moving by word, completing methods and types and the call shown above the line, that typed text stays as typed, also on the card, and the Format button, and a recommendation to label the cards in expressions that are shared, as IDs differ per world. Tutorial advancements and their criteria are untouched.

Also fixed: pasting onto panels with the Wrench

Not specific to expressions, found while testing card copies: since #1721, right-clicking a Display Panel or Dynamic Light Panel with a Wrench in a config mode rotated the panel instead of pasting the configuration, because PartTypePanelVariableDriven.onPartActivated handled every wrench before the generic part code. A Wrench in a config mode now pastes; in the other modes it still rotates. Covered by GameTestsWrenchConfig.testWrenchConfigPasteOntoPanel, which fails without the fix. This is a separate commit, so it can be split out into its own PR if you prefer.

Found, not fixed here

OperatorBuilders.FLUIDSTACK_3 is built from ITEMSTACK, so the fluid with_data operator has the unique name itemstack_withdata of the item one, and both share a translation key. Everything that looks operators up by unique name (operator values, the card codec, LP elements, expression trees) gets the fluid operator, so an item with_data operator turns into the fluid one after a save and load. It is the only duplicate unique name. Fixing it changes which operator existing itemstack_withdata data loads as, so I left it for a separate change. OperatorNames uses global names to recognize registered operators, so it does not depend on this.

Deviations from the plan, and why

  • A collapsible side panel instead of extra height. I first added 40px of height, and it was clipped: at an 854x480 window the GUI scale gives 240 rows, and 1080p at auto scale gives 270, both less than 280. A 150px side panel keeps the GUI at 402x240. Because that still costs width (at 1080p auto scale, JEI's list would drop from about 112 to about 39 GUI pixels per side), the panel can be hidden, restoring the old width. It is hidden by default; the choice is remembered for the game session only. No texture change was needed; logic_programmer.png is untouched.
  • Nesting opens the nested operator right away, rather than requiring a double-click, since its inputs are almost always filled in next.
  • Action packets instead of a full-tree packet for slot edits, so the server never has to trust client refs or deep trees for those. Applying text is the exception, with the checks described under Text.
  • Labelled refs print as #limit, not "limit". Quotes are used for string constants, so a quoted label would be ambiguous.
  • Parameters are dragged onto slots, or clicked and then the slot. While dragging, the chip follows the cursor and the slot it would go in is outlined; dropping it anywhere else cancels. Clicking both stays, since it mirrors how vanilla moves items.
  • Parameter types are set with the type selector of the List element on the selected parameter, rather than a separate type picker. It cycles through the common types (Any, Boolean, Integer, Long, Double, Number, String, List, Operator, NBT, Item, Block, Fluid, Entity, Ingredients, Recipe), followed by the types of add-ons.
  • Inner-node cache identity uses a per-tree local cache instead of ids derived from card id + node path (see above).
  • Item constants come from placing a real item in the slot (phantom, not consumed). JEI/EMI ghost dragging into those slots is not wired up, since the JEI integration lives outside this repo.
  • A text parser, which was a non-goal at first. The read-only text in the panel became editable in place, instead of a separate text mode with Edit, Apply and Cancel buttons, which an earlier version of this work had.
  • No dedicated syntax for ingredients and recipes. Their serialized form round-trips exactly, so copy and paste works, but it is not meant to be written by hand (recipes contain match conditions as numbers). By hand, they are built with the operators, as in withInputOutput(withItems(@ingredients{}, [@item{id: "oak_log"}]), withItems(@ingredients{}, [@item{id: "oak_planks", count: 4}])), which an integration test evaluates.

Behavior changes players will notice

  • Left-clicking an operator input slot with an empty hand now selects it instead of clearing it. Right-click clears.
  • Non-card items can be placed in operator slots that accept items (Item or Any), where they become inline constants, and items that hold a value of the slot's type, such as buckets in Fluid slots and block items in Block slots, give that value.
  • Escape first stops typing (applying valid text, reverting text that does not parse) or cancels a slot or parameter selection; a second Escape closes the gui.
  • Ctrl+Z and Ctrl+Y undo and redo changes to an expression in the Logic Programmer.
  • With the panel shown, the LP is 146px wider. It is hidden by default, and the arrow button shows it.
  • The + of the List element is right of its type instead of in the top right corner.
  • Loading a classic operator card into the LP shows its referenced cards from the inventory, or reference placeholders, instead of value cards holding default values.

Things to decide on

  • Typed references. With slots, refs only come from cards placed in slots, or from a card that was loaded. With text, you can type #123 for any ID. Evaluation still needs that variable in the same network, so I don't think this gives access to anything new, but you should decide on it explicitly. Requiring the card in the inventory for every #id on the server would be easy to add, but it would also reject loading and re-applying a card whose referenced cards are elsewhere.
  • Applying while typing. Text is applied after a pause of 750 ms whenever it parses, also partway through: pausing after (a + b) while typing (a + b) * 2 switches the element to Addition, and then to Multiplication once the rest is typed. Each applied step is one Undo step. In the empty panel, the first pause after valid text selects an operator in the same way.
  • Typed text on the card. The set action and the card carry the text as typed, up to 8192 characters. The server does not check that it reads as the tree, since that needs the client's labels, and the LP only shows it when it does, so a modified client can only store text that is never shown. A single operator whose inputs are all cards is still written as a classic card, which has no text, so #1.add(#2) comes back as #1 + #2 from such a card. Writing an expression card for typed text instead would keep it, but would no longer give add-ons the classic card they read.
  • The lock while the text does not parse is silent. Clicks on slots, chips and tabs do nothing until the text is fixed or reverted, without saying why.

Known limitations

  • No quick fixes (for example "Parse as Integer") and no "Try" slot.
  • No inline lambdas: parameters belong to the whole card, so map(x => x * 2, l) cannot be written as such. It needs a second card, or partial application inside the slot, as in map(apply((*), 2), l).
  • Completion lists methods after a point and types after : and @. Operator names, parameters and labels after # are not completed, and there is no key to open the list without typing.
  • Only the JavaScript syntax that keeps its meaning is read, so not all of a script pastes: numbers keep ID's types (5, 5L, 1.5, and / of two integers gives an integer, so 7 / 2 is 3), -x only works for number literals (0 - x works), there are no statements, let or idContext.ops, untyped parameters can make shared names such as size ambiguous where scripts pick the method at run time, and object values are written as @item{...} rather than IS's {id_item: ...}. Printed text uses ID's names and argument order, and writes methods on strings, lists and numbers too, while scripts only have methods on objects such as items, so it does not paste into a script as is.
  • Operators that may give an operator can only be put in as a value with shift-click, which only the book mentions.
  • #id references differ per world, so a shared expression with #12 refers to whatever card has ID 12 in the other world, without a warning. Copy writes #label when exactly one card has that label, and the book recommends labels for shared expressions.
  • Section signs in NBT strings and other serialized values do not survive pasting, as Minecraft's text fields drop them and SNBT has no escape for them. Strings in the expression itself escape them.
  • When a loaded classic card references cards that are not in the inventory, the types of refs in Any slots are unknown, so related type errors only show once the card is put back in. Network-side validation still checks them. Expression cards store these types, so this only affects classic cards.
  • An expression card used as a reference in another expression has the type inferred without its own references, so a card such as #5 + 1 counts as Any there, and type errors involving it only show in the network.
  • The panel's open/closed state is not saved between game sessions.
  • Encoding/decoding (Allow materialized values to be encoded into strings and decoded again #1737): an expression operator value serializes through its own operator serializer, which the gametest round-trips. I did not separately test the Materializer encode/decode flow with an expression card.
  • No new optional advancement for writing a card with a parameter.

API additions for add-on authors

  • Value types and operators that add-ons register in ID's registries work in expressions without extra code: as parameter types, as typed values, in slots (items are read through the value type's own ValueTypeItemStackLPElement, if createLogicProgrammerElement returns one), and by their interact names, also in the list of methods.
  • org.cyclops.integrateddynamics.api.evaluate.expression.tree: Expression, ExpressionNode (sealed records), ExpressionParameter.
  • IExpressionVariableFacade (getExpression(), and getReferenceTypes() and getText() with a default), handler ExpressionVariableFacadeHandler with type key integrateddynamics:expression.
  • ReferencePlaceholderVariableFacade with handler integrateddynamics:reference_placeholder, only used in LP slots (phantom, cannot be taken out). In a network it resolves to the card with the same id.
  • ExpressionOperator (new IOperator implementation) with operator serializer integrateddynamics:expression.
  • ExpressionTrees (serialization, paths, queries), ExpressionTypeChecker (including the inferred type of every node, and Inference for nodes built bottom-up), ExpressionPrinter (including printExact, printLiteral and tokenizeExact), ExpressionParser (including complete and tokenize) with ExpressionParseException, ExpressionTextNavigation, OperatorNames (including getMethods), ExpressionEvaluator.
  • LP: ExpressionEditor (including getText(), the set action with its text, isEditable, parse, isParameterType and getParameterTypes), OperatorLPElement.getEditor() / applyExpressionAction(...) / getCurrentOperator() / findReferencedCards(Player) / setExpression(...) / getInexactItemConstant(slot), ValueTypeItemStackLPElement.getItemStackToValue(), ContainerLogicProgrammerBase.applyExpressionAction, refreshElementInventory, setExpressionSlotFilter, layout constants EXPRESSION_PANEL_*, ContainerScreenLogicProgrammerBase.isExpressionPanelOpen(), and RenderPattern.getRenderSymbol().
  • LabelsWorldStorage.getVariableIds(label).
  • IOperatorVariableFacade is unchanged and expression facades do not implement it, so add-ons reading it see exactly the cards they saw before.

Testing

  • ./gradlew build: passes, 1030 unit tests. New:
    • TestExpressionTrees (55 tests: serialization round trip, 60-level tree and its binary size, too-deep and invalid-index rejection, paths, compatibility rule, evaluation equivalence with a LazyExpression chain, invalidation, parameter binding, apply/filter/map/reduce/pipe/flip/currying, lazy choice, operator serialization, materialization including inside pipe, type inference and error positions, the card's type with and without stored reference types, printer output including shared names told apart by type, former LP symbols written as calls, and + for concat only when it reads back as concat; and more complex cases: three parameters with partial application, apply_2 and flip, higher-order parameters, expression operators as inline values and as refs, map(apply((*), 2), l) as an inline doubler, nested materialization with live and frozen refs, a 30-level parameter chain, a parameter used in several subtrees, and an evaluation error deep in the tree).
    • TestExpressionEditor (14 tests: actions, parameter type inference including Number, picking operators as values, value parsing, rejection of bad actions, the set action and what cannot be edited, the 1024-node limit of typed text with a 1023-node and a 2047-node balanced tree, and adding a parameter to text: in front of text without parameters, after one with or without brackets, with types and namespaces, before an unfinished rest, and not beyond 8; the text of the set action, without text that is too long; and the text kept through edits that leave the tree as it is, and dropped by each kind of edit that changes it).
    • TestOperatorNames (4 tests, including that every registered operator gets an identifier name that resolves back to it, with and without a known first argument type, and that + is concat with a string input and (+) addition).
    • TestExpressionParser (24 tests: every editable operator, more than 250, prints and parses back to itself; the display form also parses; values, including NBT and a curried operator, and refs and labels print and parse back; precedence, parameters, type-dependent names, literals and errors with positions; depth and node limits; and the Integrated Scripting syntax: both arrows, method calls with their precedence, chains, receivers and errors, two examples from the IS book, calling operator parameters with 0 to 3 inputs and the error for 4, === and !== also as operator values, single and curly single quotes in strings and labels, + as concat when either input is a string and as addition otherwise, concat printed with + only when it reads back as concat (also as a value and for untyped inputs), a 2001-term chain and a 400-call method chain giving their depth error within a timeout, and which calls are printed as methods, read back the same: on parameters, literals, references, chains, and receivers with infix and prefix symbols in brackets, but not with global names, for operators that take an operator first, on operators, empty slots and numbers, or for incomplete choices; and the names of types, with item and fluid printed and their ids read; and the tokens of typed text: methods, references, spaces outside the expression, ternaries, brackets that belong to the value in them, a minus that belongs to its number, strings with their quotes, a called parameter, empty slots told apart by their order, and operators as values, each making up the text and giving the parsed tree). Without the span of brackets, the tokens test fails. Breaking the L suffix on purpose makes two of them fail.
    • TestExpressionValueLiterals (15 tests: full round trips, through the printed expression and back to an equal expression and the same text, of booleans, integers, longs and doubles including their extremes, infinities and negative zero, strings with every escape, unicode, and the characters that pasting drops, operator values including a curried one, NBT of every tag type, of tags other than compounds and of no NBT, entities, and lists of every value type above, nested, mixed, empty and the ones that keep the @list form; reading list literals with a trailing comma and over several lines, and their errors; typed values that would be read as something else; the containment check itself, including numbers of another type with the same value in values, lists and arrays, and numbers that change, such as 1.5 read as 1 or 300 as 44b; pasted text with non-breaking, thin and zero-width spaces, curly quotes in strings and labels, and Windows line breaks; and too long text).
    • TestExpressionAddonTypes (4 tests, with value types of fake add-ons that, like Integrated Mekanism's chemicals, have their own namespace and read an unknown id as an empty value: parameters typed with and without the namespace, printed with it and read back, an ambiguous short name and an unknown namespace as errors; values printed and read back, with the short name and an Integer amount, and an unknown id as an error; the editor's parameter types and their order; and setting a parameter to that type in the editor). Disabling the namespace parsing and the number comparison makes 3 of the new tests fail.
    • TestExpressionTextNavigation (6 tests: the word at a position, for names, numbers with a point, symbols, brackets, labels, typed values, strings and past the end; stepping through item.size(x) >= 16 by word in both directions, across spaces and line breaks; bracket pairs, skipping brackets in double, single quoted and escaped strings, with braces of typed values, and brackets that do not match; which bracket is used next to the cursor; and the keys, with Ctrl and Alt elsewhere, and Option, Cmd and both together on macOS).
    • TestExpressionCompletion (5 tests: the methods of a type, including those of its categories, ordered by name, with each name read back as its operator for every registered type; the methods after a point after parameters, calls, literals, references, brackets and in a ternary, and none for the point of a decimal number, in strings, after a space or after errors; the innermost open call and the input at the cursor, for methods and calls, also while typing a method, and none after the call, for shared names before the first argument, for parameters, unusable operators and after errors; the type being typed after the colon of a parameter, also of a later one, with a namespace, and in the middle of a name, and after @, but not after a complete type, a space or the parameters, without a colon, in a ternary or in a string; and which types are listed: by prefix, ignoring case, also without the namespace of an add-on, ordered with those of ID first, items and fluids by their name and id, only parameter types for parameters, and no categories for values).
    • TestExpressionHistory (5 tests, including the same tree typed in another way as a change) and TestExpressionTextField (6 tests, for the colors kept around typed text, and where the signature popup goes: above the line, below the first line, moved left at the edge of the screen and cut off when wider).
  • ./gradlew runGameTestServer: all 1067 pass. New GameTestsExpressions: ref + inline constants on a Display Panel, equality with the equivalent classic card chain, item => size(nbt(item)) > 1 used by a classic filter card over an Inventory Reader list, apply of an expression operator with a ref, an expression operator on a Display Panel (serialized and evaluated), card data save/load, a missing reference, the LP compatibility rule and loading old/new cards, items in slots of other types (a water bucket in a fluid slot, a typed 250 mB of lava that keeps its amount when its bucket is put back, taking the bucket out, a stone item in a block slot, and a diamond in neither), a type error position in the LP, l => reduce(#add, map(#double, l), 0) where #add and #double are expression cards in the same network, three parameters partially applied through apply, a Display Panel following a changed referenced card, two expression cards referencing each other and one referencing itself (validation error, no crash), reference types surviving an LP write and load, copying expression cards with and without parameters in a crafting grid, pasting one onto another Display Panel with the Wrench (the copies get a new id, keep the expression and reference types, and evaluate like the original), applying (#id + 3) * 2 as text to an LP on > (Arithmetic Multiplication becomes active, a const root is ignored, and the written card shows 36 on a Display Panel), applying text keeping the open operator, applying text while no element or a value element is active (the element of the root operator becomes active), and typed text kept by the LP, on the written card and its copy, and when the card is loaded, and no longer after a slot changes the tree. New integration tests in TestExpressionValueLiteralsInGame (10, run as game tests, for the values that need the game's registries): round trips of items (a stack, more than 99, empty, and a sword with damage, a quoted custom name and an enchantment), blocks with and without properties and air, fluids and an empty one, ingredients with items, fluids and energy, recipes with alternatives and an empty input, and lists of all of these; an item without a count or namespace; unknown items and blocks, a count of 0 and an invalid block property as errors; and withInputOutput(withItems(@ingredients{}, [...]), ...) evaluated to a recipe with the right output. I confirmed the new classes actually run by breaking one assertion once and seeing it fail, and saw the new integration tests fail and then pass while writing them.
  • Building expressions with slots, with clientdevbridge-cli: built item => size(nbt(item)) > 3 from scratch, changed a parameter type to cause a nested type error, cancelled with Escape, entered an invalid value, wrote the card, and hid and reopened the panel. Built (x, y, z) => x > y ? x * z : y - z, l => reduce((+), map((stringLength), l), 0) (checked the written card's tooltip: List -> Integer) and x => x > 1 ? x * 2 : x (x inferred as Number), and caused a branch type mismatch in choice to check that its message is shown. Built #greeting == (#greeting + "!") with a String card, navigated in and out (the actual card stays), wrote it, and loaded it back with the card in the inventory (actual card shown) and with the card moved to a chest (placeholder with id and type shown). On an earlier UI revision (the network side is unchanged since): built filter(#bignbt, #items) (written as a classic card) and length(filter(#bignbt, #1)) in a real network with an Inventory Reader, Variable Store and two Display Panels, reloaded the world (both panels kept [Sword] and 1), used the Labeller, made an item constant in the Portable LP, and checked that the List and Recipe elements are unchanged. Built x => getInteger(nbt(x), "key"), which shows itemstackNbt while the nested slot is empty and nbt once x is in it. Copied the (x, y, z) card in a crafting table (the card left in the grid gets a new id, both keep the expression), and copied a Display Panel holding it onto another one with the Wrench (pasted with a new id, after the panel fix above).
  • The panel and text, on the current version (screenshots above; the ones before e245594 print ->, now =>, and the ones before 71eeee7 print calls such as size(item), now item.size(), and the ones before b0df4cb print itemstack and fluidstack, now item and fluid, and the ones before 39ef904 show typed text printed once the text lost focus, which now stays as typed, and the ones before ba5e4c0 show the call at the cursor in the footer, which is now in a popup above the line):
    • the panel hidden by default, the arrow showing it, and staying shown after reopening the gui
    • the type in its colors (Integer, Double, String, Number, and signatures such as Integer -> String -> String), a wrapped type, a slot error, and the full message on hover
    • parameters: none, three, 8 with 16-character names, a long name that is cut off and wraps, the tooltip with only name and colored type, dragging one onto a slot (the chip follows the cursor and the slot is outlined), dropping it onto nothing, and clicking a chip and then a slot
    • the type of a parameter set with the arrow buttons, three steps forward and one back, while the name field kept its caret, then renaming it with Enter; the name and type filling the row with a margin of 2 pixels on both sides, measured in a screenshot; and a selected slot's hint and error next to the same field
    • the List element after moving its +: the + adding an entry, and the panel's arrow button hiding the panel
    • clicking the text placing the caret and opening that operator in the slots, and the operator list showing that operator as selected, also after going back to the root, with a click on it doing nothing
    • typing being applied after a pause without Enter, with the card rewritten, Enter applying right away, and the open operator staying yellow around typed text
    • y+1 staying as typed while focused and printed as (x * y) + 1 once it lost focus (before 39ef904, it now stays as typed)
    • text that does not parse being underlined and not applied, slot clicks being ignored meanwhile, and Undo (the button) and Escape reverting it without changing the expression
    • Escape right after typing + 4 applying it, in the panel of an expression and in the empty panel
    • closing the gui right after typing applying the text, through onClose called directly, as clientdevbridge's own close bypasses it
    • selecting by dragging, and replacing everything with (a: integer) => a + #5, which switched to Arithmetic Addition with the real #5 card in its slot and rewrote the card
    • appending + 3 to an applied (input: integer) => input + #N: with an Integer card #5 typed, the same card placed in its slot by hand, and an ID without a card (#9), both while the text still had focus and after clicking it again. Each gave input => (input + #N) + 3.
    • with no operator selected: the placeholder on three lines, its box gaining focus, a parse error while typing (input: integer) => input +, the type once #5 was added, Arithmetic Addition becoming active after the pause with the real #5 card in its slot and the text still focused, continuing with + 3, and a card written from (n: integer) => n + #5 reading n => n + #5, Integer -> Integer
    • values: a cable placed in a slot copied as @itemstack{count:1,id:"integrateddynamics:cable"}, a block literal applied and copied back the same, l => l == [“apple”, “pear”] with curly quotes read as a list of Strings and copied as l => l == ["apple", "pear"], and a misspelled item shown as Not a valid itemstack value, it would be read as {} instead of turning into an empty slot as it did before
    • the Integrated Scripting syntax, typed in the empty panel: (item: itemstack) => item.isStackable() && item.size() >= 16 (Logical And, Item -> Boolean, printed as isStackable(item) && (size(item) >= 16)), (name: string) => 'Hello ' + name + '!' (String Concat, String -> String, printed as ("Hello " + name) + "!"), and (f: operator, x: integer) => f(x) === 1 (printed as apply(f, x) == 1)
    • a water bucket in the slot of Fluid Amount copied as amount(@fluidstack{amount:1000,id:"minecraft:water"}), and a typed 250 mB of lava shown as a lava bucket, whose tooltip reads Lava Bucket and Lava (250 mB)
    • with (item: itemstack) => item.isStackable() && (item.size() >= 16) typed: the outer parentheses highlighted with the cursor after the last one, a double click on isStackable selecting only that name, a third click selecting everything, and a double click on item in (item.size() selecting only item. The clicks were sent to the screen within one script, as clientdevbridge's separate clicks are further apart than a double click.
    • completing methods: item. listing the methods of an item over the name bar of the element, si leaving size, Enter inserting size(), Tab inserting strength( with the footer showing Item.strength(Block): Double, a click inserting canHarvest(, the mouse wheel moving to encode, Escape closing only the list, and clicking inside a call showing it in the footer, in the empty panel and in the panel of an expression
    • completing types: (x: int listing integer Integer, Enter inserting integer, , i: ite listing itemstack Item, @ite after != listing the same, and, after applying (x: integer, i: itemstack) => i.size() > x && i != @itemstack{id: "diamond"} and clicking outside the text, the text printed as (x: integer, i: itemstack) => (i.size() > x) && (i != @itemstack{count:1,id:"minecraft:diamond"}), keeping i.size()
    • the colors in the list: after (x: the types with their names in their colors (Boolean blue, Double yellow), item and fluid under their new names, i listing ingredients, integer and item, and the methods of a list with the types of their inputs and outputs in color, on a gray selected entry. List's dark red is the hardest to read on the dark background, as it is in ID's tooltips
    • the panel without an operator showing Undo, Redo, Copy and + Parameter with the text box below them, + Parameter twice giving (x, y) => with the cursor at the end, Undo emptying it, and (x: item) => x.fluidStack().block().breakSound() applied without the text moving; clicking fluidStack giving the breadcrumb breakSound › … › fluidStack; and clicks across the 4 pixels around the boundary between the brackets of x.fluidStack() placing the cursor between them, with the footer showing Item.fluidStack(): Fluid (only a click on the closing bracket did before)
    • typed text kept as typed: (x: integer) => x.add(3) typed in the empty panel staying as typed after it was applied and the text lost focus, Format giving (x: integer) => x + 3 and disabling itself, and Undo bringing back x.add(3); (x: item) => ((x.size()) > 16) && !x.isStackable() keeping its brackets and spaces, with && in the color of the open operator, a click on size opening it in the slots with (x.size()) highlighted, clearing a slot printing (x: item) => (x.size() > 16) && ?, and Undo bringing back the typed text; a card written from it on the server and loaded back after Clear showing it as typed; and Format in the panel without an operator replacing typed text that was not applied yet
    • the signature popup: below the first line for (x: item) => x.strength(, while the footer showed the parse error, with Block underlined; above the second line of (x: item) => x.strength(@block{Name:"stone"}) > 1.5 && x.size() >= 16 with the cursor after strength(, covering the first line, and above the third with the cursor in x.size(), showing Item.size(): Integer, while the footer kept showing Item -> Boolean; and hidden while the list after x. inside the call was shown, coming back once Escape closed the list
    • the tooltip of cards written from text matching the panel: a => a + #5 as Integer -> Integer (it said Integer -> Any before the reference types were used), and #5 + 1 as Integer
    • undo and redo with the buttons, over text changes, slot changes and a change of root operator
    • Clear or loading a card resetting the history
    • Copy giving the exact text, and its check
    • scoped names (length, concat) and string literals
    • the margins, measured in a screenshot
  • Not exercised in the client: Ctrl+Z, Ctrl+Y, Shift+Enter, paste, and moving or deleting by word or line with Ctrl, Option or Cmd. clientdevbridge sends modifier bits, but vanilla checks the physical key state with Screen.hasControlDown(), Screen.hasShiftDown() and Screen.hasAltDown(), so I used the buttons instead, and the keys are covered by the unit tests of ExpressionTextNavigation. The macOS keys could not run in game at all, since the dev client runs on Linux. What pasting does to text (it goes through StringUtil.filterText, which drops the section sign and control characters, and is cut off at the field's limit) is from vanilla's code, and covered by the escaping and length tests above.
  • Integrated Mekanism 1.2.0 with Mekanism 10.7.16 in the dev client. It needs NeoForge 21.1.238 and CommonCapabilities 2.11.6, while this branch builds against 21.1.228 and 2.9.11, so I used a local copy with relaxed version ranges (all 35 CommonCapabilities classes it uses exist in 2.9.11), without its 3 game tests that need Integrated Crafting or other add-ons, and with its JEI drag converter stubbed, as that needs ID's compat classes, which are only shaded into release jars. Before the add-on commit, a chemical literal and amount worked, but a parameter typed integratedmekanism:chemicalstack did not parse, amount: 1000 was rejected, and a parameter dropped in a chemical slot showed Any in the type selector. After it: (c: integratedmekanism:chemicalstack) => c.amount() > 1000L (Chemical -> Boolean, the selector showing Chemical, and cycling back to Recipe giving a type error and the global name chemicalstackAmount(c)), (c: chemicalstack) => c.amount() > @chemicalstack{id: "mekanism:hydrogen", amount: 1000}.amount() printed with namespaces and 1000L, a Basic Chemical Tank in the slot of Chemical Amount giving @integratedmekanism:chemicalstack{} (it was empty), and a water bucket not accepted in that slot.
  • Integrated Tunnels 1.13.0-801 (from the CyclopsMC Maven, in the dev client's mods folder): item => length(name(item)) > #threshold in an Item Importer's Import Items Predicate aspect, with the threshold an Integer card in a Variable Store. With single items, Diamond, Cobblestone and Oak Planks were imported and Apple, Stick and Egg stayed. With the threshold card taken out of the network, the importer showed "Variable with id 5 could not be found within the current network" and an added Emerald stayed; after putting the card back, the Emerald was imported. With stacks, note that ID's name of an item stack includes the count ("Apple (10)"), which is existing behavior. Screenshots, taken before the panel changes: https://claude.ai/artifact/WZMMY6VcHoVUWMW4d27N42 (a private page owned by @rubensworks; GitHub's API does not accept image uploads, so share or copy them from there).

Issues

Closes #194, closes #546, closes #860, closes #1112, closes #1417, closes #1495, closes #367 (these are also in the commit message). Partially addresses #193, #216, #527, #804, #930, #1293 (type shown before writing, errors pointing at the slot) and #43, #906, #1326, #525, #1130 (fewer intermediate cards).

🤖 Generated with Claude Code

https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y

An expression variable stores a tree of operator applications,
references to other variables, inline constants and parameters.
Without parameters, it evaluates to the value of its root, using cached
lazy expressions per node. With parameters, it evaluates to an
expression operator that can be passed to apply, map, filter, reduce,
pipe, flip and other operator consumers.

Includes type inference with structured errors per slot, and a
pretty-printer used in tooltips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Operator elements now edit an expression tree. A slot accepts a
variable card, a typed value, an item, a parameter, or a nested
operator picked from the list while the slot is selected, which filters
the list by the slot type. Nested operators open in the same config
area with a breadcrumb to go back up. A side panel shows parameters,
the pretty-printed expression with errors highlighted, the inferred
type and what is still missing. Type errors outline the failing slot.

A single operator with only variable cards is still written as a
classic operator variable, so existing cards and add-ons are unaffected.

Suffix operators with word-like symbols are now printed in prefix
notation, so item NBT size prints as size(nbt(item)).

Closes #194
Closes #546
Closes #860
Closes #1112
Closes #1417
Closes #1495
Closes #367

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Covers an expression with references and inline constants on a Display
Panel, equivalence with a chain of classic cards, an expression
operator used by a filter over an Inventory Reader list, apply with
references, persistence of the card data, and how the Logic Programmer
writes and loads plain operators and expressions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
@coveralls

coveralls commented Oct 16, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 48.358% (-0.3%) from 48.683% — feature/expression-cards into master-1.21-lts

- Draw chips with short operator names at the largest scale that fits,
  and mark unfilled nested operators with a badge.
- Show what to do next in the expression panel, depending on the
  selection and the state of the expression, instead of small hints
  in the config area and long slot tooltips.
- Open a nested operator right after picking it.
- Let selected parameters cycle through value types.
- Make the expression panel collapsible, like the recipe book, so the
  Logic Programmer keeps its old width for players who don't need it.
- Escape and clicking the empty config area cancel a selection instead
  of closing the gui.
- Outline chips whose nested operators contain a type error, and name
  the operator in the error status.
- Draw the breadcrumb as tabs, allow empty string values, and use
  readable colors for the type signature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Loading cards into the Logic Programmer:
- Store the output types of referenced cards in expression cards, so
  type checking works after loading a card back into the LP.
- Show the actual referenced cards when they are in the player's
  inventory, otherwise a reference placeholder card with the id and
  type, instead of value cards with default values.
- Remember cards placed in slots while navigating through nested
  operators. Before, navigating back replaced them by placeholders
  whose default type could corrupt the known reference type.
- Infer missing reference types from concrete operator input types.

Expression editing:
- In Operator slots, list all operators. Operators that cannot give an
  operator are inserted as values, printed as (+). Shift-click inserts
  any operator as a value.
- Print complete choices as c ? a : b, and don't show choice as "?".
- Infer parameter types from Number slots as well.
- Show errors of an operator as a whole in the hint, place the hint
  below the status, and clarify the hint texts.

Tests for complex expressions: three parameters with partial
application, higher-order parameters, expression operators as values
and refs, nested materialization, deep parameter chains, expressions
composed from expression cards in a network, live updates of
references, reference cycles, and reference types surviving an LP
write and load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Right-clicking a Display Panel or Dynamic Light Panel with a Wrench
always rotated the panel, so a configuration copied with #1721 could
not be pasted onto a panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Copy expression cards with and without parameters in the crafting
grid, and paste one with the Wrench. The copies get a new id, keep
their expression and reference types, and evaluate like the original.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Print operators by their scoped interact name, such as length or nbt,
and fall back to their global interact name, such as stringLength,
when the scoped name is shared by operators that the type of the first
argument does not tell apart. Only + - * / % == != < <= > >= && || and
! keep a symbol, so names no longer contain characters such as
NBT{}.get_integer or =NBT=, and every printed name leads back to one
operator. Chips, breadcrumb tabs and status messages use the same names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The expression panel gets Edit, Copy and Undo buttons. Edit turns the
expression into text in the format of Integrated Scripting, which is
parsed and type checked on the client while typing. Applying sends the
resulting tree to the server once, as a single set action that the
server validates, and makes the element of the new root operator active
when it changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
claude added 14 commits October 17, 2026 06:04
Drop the hint below the status and the "Ready to write" line, which the
check mark next to the operator name already shows. Errors of an operator
as a whole, which have no slot to hover, now show their message as the
status. The type is drawn below the expression on its dark background, in
the colors of the value types as in card tooltips. The button that adds a
parameter is a regular button like the others in the Logic Programmer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Brings in the simplified expression panel. Edit, Copy, Undo, Apply and
Cancel are now regular buttons in a row at the bottom of the panel, like
the other buttons of the Logic Programmer, instead of drawn boxes in the
title. The hint below the text box is gone with the other hints, and the
type of the typed text is shown in the colors of its types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The dark box of the expression now reaches the bottom of the panel, with
a footer that shows the type, or the first type error instead, so there
is no empty area left for messages that may come. Hovering an error shows
its full message. The expression and the footer are drawn at full size,
since text scaled to 75% gets uneven strokes at most GUI scales. The
empty slot count and depth note are dropped, empty slots already show
as ? in the expression.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The text box ends right above the footer of the panel, which shows the
type of the typed text, or the parse error with its line and column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Parameter chips past the second row were not drawn, so with long types
or names some parameters could not be selected, renamed or removed.
Chips now show only the name, at full size and at most half the panel
wide, and wrap onto as many rows as needed, with the expression below
them. A chip's tooltip names its type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Besides clicking a parameter and then a slot, a parameter chip can now be
dragged onto a slot. While dragging, the chip follows the cursor and the
slot it would go in is outlined. Dropping it anywhere else cancels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The text of the expression in the panel can now be edited directly,
instead of switching to a separate text box with an Edit button. It keeps
the colors of the read-only view, and clicking a part places the caret
and opens that operator in the slots. Typing makes a draft: Enter applies
it, Escape reverts it, and slots wait until then. Applying keeps the open
operator if it is still there.

Undo and Redo go back and forth through all changes to the expression,
from slots, parameters and text alike, and are reset when another
expression is opened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The font has no undo, redo or copy symbols, so the icons are drawn
pixel by pixel, in the same colors and with the same shadow as the text
of text buttons. Copying briefly shows a check mark instead of the
"Copied" text. The hint when hovering the expression is removed, as
the button tooltips and the book already explain the editor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
With icons, Undo, Redo and Copy fit right of the title, so the row of
buttons below the expression is removed and the expression gets that
height instead. Revert and Apply become a cross and a check there, with
tooltips naming Esc and Enter. The add parameter button moves down a
pixel so its plus is in line with the Parameters label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The label sat right under the title like a second heading, while
parameters are optional. The chips now sit under the title on their
own, followed by a plus to add another, which reads "+ Parameter" while
there are none. Chips are as tall as the button, so they line up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The main texture covers the left of the panel, so its content had a
margin of 2 pixels on the left and 3 on the top and right. It now starts
one pixel further right. The footer shows only the type, recognizable by
its colors, and names it as the type in its tooltip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The type is recognizable by its colors, so the tooltip only got in the
way when moving the mouse over the footer. Errors and text that does not
fit still show a tooltip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The whole text turned plain as soon as a character was typed, which hid
the open operator and the empty slots. The text before and after what
was typed now keeps its colors. Errors are left out while typing, as the
footer then shows the errors of the draft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
@rubensworks rubensworks changed the title Expression cards in the Logic Programmer Expression cards in the Logic Programmer, editable as text Oct 17, 2026
The tooltip of an expression card inferred its type as if every
referenced card could be of any type, so a -> a + #5 showed
Integer -> Any, while the Logic Programmer showed Integer -> Integer.
The card already stores the types of the cards it references, and the
tooltip now uses them, like the expression line above it. The type that
networks validate against stays as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Comment thread src/main/resources/assets/integrateddynamics/lang/en_us.json Outdated
claude added 19 commits October 18, 2026 12:18
The Logic Programmer keeps its old width until the arrow button at its
top right opens the panel, which is then remembered for the rest of the
game session. The book now starts from that button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Applies the suggestion from the review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The type is drawn in its own color, as in other tooltips. How to use a
parameter is explained in the book.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The list showed the root operator as selected while a nested one was
open in the slots. It now shows the open operator as selected, as it
does for the operator of a loaded card. Clicking that entry does
nothing, as activating it would replace the whole expression.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Typed text is applied once it parses and typing pauses for 750 ms, or
right away with Enter, so the card is rewritten without confirming.
The text stays as typed while it has focus, and is printed in its usual
form once it loses focus. Clicking outside the text, hiding the panel
or closing the gui applies valid text first. Undo, Redo and Copy stay
visible while typing, and Undo reverts typed text that is not applied
yet, so the apply and revert buttons are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
- With no operator selected, the expression panel takes typed text.
  Applying it selects the element of its root operator, where typing
  continues. The set action now also works without an active operator
  element.
- Escape applies valid typed text instead of reverting it, so the
  result no longer depends on whether it was pressed within the apply
  delay. Text that does not parse is still reverted.
- The arrows of the parameter type stay in place while cycling, as the
  type name gets the width of the longest one.
- Typed expressions with more than 1024 nodes get an error, instead of
  being applied and then ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The type of a selected parameter is now right-aligned in the config
area, as wide as the widest type name, and the value field takes the
rest of the row, so both together span its full width. Slot values use
the same field width, so it does not change between them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The type of a selected parameter is now a WidgetArrowedListField, the
selector the List element uses for its type, instead of drawn < and >.
The value field next to it gets the same gray background, and both are
as high as that selector. ExpressionEditor.cycleParameterType is
removed, as the selector cycles itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The button to show the expression panel is in the top right corner of
the config area, where it covered this button and took its clicks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The hint above the text box repeated the arrow to the operator list,
and described the slots, which only show once an operator is picked.
The placeholder in the box now says both, and the box starts right
below the title, where the parameters start once an operator is picked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
- Values written with their type, such as @ItemStack{...}, are read
  back and compared with what was typed. Values that fall back to a
  default for what they cannot read, such as an unknown item, an empty
  list for a malformed one, or an empty entity for a bad UUID, now give
  an error that shows what they would be read as, instead of silently
  becoming something else. Defaults that are added are allowed, and ids
  can leave out the minecraft namespace.
- Lists are written as ["apple", "pear"], [1, 2] or [[1], [2, 3]], with
  the type the values share, or Any. Lists whose type their values do
  not give, such as an empty list of Strings, keep the @list form.
- NBT is written as the tag itself, as in commands: @nbt{a:1b}, and
  @nbt() for no NBT, instead of the internal @nbt{v:{a:1b}}.
- An item without a count is a single one.
- Pasted text: other kinds of spaces, such as non-breaking ones, are
  whitespace, curly quotes work as quotes, strings escape the section
  sign and other characters that pasting drops, and text that is too
  long gives an error instead of being cut off.
- Unit tests for the round trips of all values that do not need the
  game's registries, and integration tests for items, blocks, fluids,
  ingredients, recipes and lists of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Simple functions from Integrated Scripting can now be pasted as
expressions, with at most their parameter types added:

- Parameters are declared with =>, which is also what is printed.
  -> is still read.
- item.size() can be written for size(item), as for the methods of
  values in scripts. It is printed as a call.
- A parameter that holds an operator can be called, where f(a) is
  apply(f, a), up to apply3 for three inputs.
- === and !== are read as == and !=, which already compare strictly.
- Strings and labels can be in single quotes, also curly ones.
- + joins strings when one of its inputs is a string, as concat. It is
  only printed for concat when it would be read back as concat.

As + needs the types of both inputs while parsing, the parser now
infers the type of each node once, instead of type checking the first
argument of each call again, so long chains such as a + b + c stay
linear.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Value types of add-ons, such as the chemicals of Integrated Mekanism,
only partly worked in expressions:

- Parameters could only have the types of Integrated Dynamics, from a
  fixed list used by the type selector, the parser and the server.
  Dropping a parameter in a chemical slot still made it a chemical,
  which the selector showed as Any. Parameters can now have any
  registered type that is not a category, plus Any and Number, and the
  selector lists the types of add-ons after those of Integrated
  Dynamics.
- (c: integratedmekanism:chemicalstack) did not parse, as the colon of
  the namespace ended the parameter list, while the printer writes the
  types of add-ons this way. Types can also leave out their namespace
  when no other type has that name, as in @chemicalstack{...}.
- Typed values compared numbers by their tag type, so amount: 1000 was
  an error for an amount that is stored as a long. Numbers with the
  same value now count as kept, also in lists and arrays.
- Slots only took items in Item slots. They now also take the items
  that the element of the slot's type reads, such as a bucket in a
  fluid slot, a block item in a block slot and a chemical tank in a
  chemical slot. Such a constant is shown as that item, keeps a typed
  amount when the item is put back, and the item's tooltip shows the
  value when the item does not show it exactly.

Checked in game with Integrated Mekanism 1.2.0 and Mekanism 10.7.16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The text of expressions used vanilla's text field behavior, where a word
only ends at a space, so item.isStackable() was one word, double-clicking
did nothing, and on macOS Cmd moved by word while Option moved by one
character.

- Double-clicking selects a word, and a third click the whole text.
- Ctrl with Left, Right, Backspace or Delete moves or deletes by word,
  where words end at names, numbers, symbols and brackets.
- On macOS, Option does that instead of Ctrl, and Cmd goes to the start
  or end of the line, or with Up and Down of the text, as in its own
  text fields.
- The bracket next to the cursor and its match get a background,
  skipping brackets in strings.

The word, bracket and key logic is in ExpressionTextNavigation, with
unit tests for both platforms, as the dev client cannot hold modifier
keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
While typing a name after a point in the expression text, as in
item.si, a list shows the operators that can be called on the value
before the point, with their other inputs and output. The value's type
comes from the type checker, so this works after parameters, calls,
literals, references and brackets. Up and Down choose a method, Tab,
Enter or a click inserts it with its brackets, and Escape closes the
list. The list closes when the cursor moves in any other way than
typing, as in Integrated Scripting.

While the cursor is inside the brackets of a call, the footer shows its
inputs and output in the colors of their types, with the input at the
cursor underlined, as in Item.strength(Block): Double.

Both come from parsing the text up to the cursor, which records the
calls whose brackets are still open and the type of the value before a
point at the end. The methods of a type are the operators whose first
input takes it, named as the printer names them, so each name is read
back as the listed operator.

The list is drawn after the rest of the gui, as slots and labels are
drawn without depth and would cover it, and hides tooltips of slots
below it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Typed text such as item.size() became size(item) once it lost focus.
The card only stores the tree, so the text is printed again from it,
and the printer wrote every operator as a call, as it did before the
method syntax was added.

Calls that use the scoped name, which the type of the first argument
tells apart, are now printed as methods of that argument, as in
item.nbt().size() > 3, which is how Integrated Scripting calls the
methods of values. Calls with a global name stay calls, as in
stringLength(x), and so do operators that take an operator first, such
as map(f, l), incomplete choices, and calls on an empty slot or a
number, where 1.max(2) would read as a decimal number. A receiver with
symbols gets brackets, as in (a + b).length().

Two error messages gave x => length(x) as an example, which does not
parse, as length is shared by strings and lists. They now give
(s: string) => s.length().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
While typing the type of a parameter, as in (x: int, or of a value, as
in @ite, the list that shows methods after a point shows the types
whose name starts with what is typed, or whose name without the
namespace of an add-on does, with the name the game gives them next to
it. Parameters only list the types they can have, and values no
categories. Inserting one replaces the typed name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The text names types by their id, so items and fluids were itemstack
and fluidstack, while the game calls them Item and Fluid. They are now
written as item and fluid, as in (i: item) => i.size() and
@item{id: "minecraft:diamond"}. Their ids are still read.

The global names of their operators, such as itemstackSize, still use
the ids, as scripts of Integrated Scripting call them by these names.
TODOs mark them to be renamed in the next major version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
The types in the list, such as Integer next to integer and the inputs
and output of methods, are shown in the colors of their types, as in
tooltips and in the footer. The selected entry gets a gray background
instead of a blue one, on which the blue of Boolean was hard to read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
While no operator was selected, the panel only showed the text. Undo,
Redo, Copy and + Parameter appeared once the typed text was used,
which moved the text down. The panel without an operator now has them
in the same places, so it looks the same before and after.

As there is no expression yet, they work on the text: Undo reverts it,
Redo has nothing to redo, and + Parameter adds a parameter to the text,
as x => in front of it, or after its other parameters. In the panel of
an expression, + Parameter was hidden while typed text was not used
yet, and now adds to that text in the same way. Both panels share their
buttons in ExpressionPanelButtons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
With a chain such as x.fluidStack().block().breakSound(), the tabs
above the slots ran past the config area. The tabs in the middle that
do not fit are now collapsed into one tab with an ellipsis, as paths
are shortened. It leads to the last operator it stands for, and its
tooltip lists them. Names that still do not fit are cut off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Vanilla's text model places the cursor before the character under the
mouse, so a click on the opening bracket of x.fluidStack() put the
cursor before it, and only a click on the closing bracket put it
between them, where the footer shows the call. The cursor now goes to
the closest boundary, as in other editors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
Typing (x: integer) => x.add(3) showed x + 3 once the text lost focus,
as the text was printed again from the tree. Typed text is now kept:
in the editor, through undo and redo, and on the written card, which
stores it next to the tree. It is only shown while it still reads as
the same tree, and otherwise the tree is printed as before, such as
after a label it uses changed. A slot or parameter edit that changes
the tree prints it from then on. Text in the usual form is not stored,
and plain operators are still written as classic cards, without text.

ExpressionParser.tokenize records where each node was read, so typed
text maps each character to its node, as printed text does, for its
colors, highlights and opening the node under the cursor.

A Format button between Redo and Copy writes the expression in its
usual form, applying typed text first if it parses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y
While the cursor was inside the brackets of a call, the footer showed
the signature of that call instead of the type of the expression, so
it was unclear which of the two it showed, and errors of the
expression were hidden meanwhile. The signature is now shown in a
popup, as in Integrated Scripting's editor: above the line of the
cursor, starting at the left of the text, below the first line as
there is no room above it, and not while the completion list is
shown. The footer always shows the type or errors of the expression.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHtucHo4Qy2pTfBMgeE54y

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants