Repository navigation
Expression cards in the Logic Programmer, editable as text - #1766
Open
rubensworks wants to merge 52 commits into
Open
rubensworks wants to merge 52 commits into
rubensworks wants to merge 52 commits into
Conversation
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
- 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
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
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
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
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
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
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
rubensworks
commented
Oct 18, 2026
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
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.
One variable card can now hold a whole expression built in the existing Logic Programmer, for example
instead of 6 to 8 cards in a Variable Store glued together with
operator,apply,pipeandflip. 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 sealedExpressionNode:Operator(op, children),Ref(variableId),Const(value),Param(index), plusEmptyfor slots that are still being edited.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.text), if any, which only the LP uses (see Text).Evaluation
ExpressionVariableFacade.getVariablebuilds nestedLazyExpressions. The root is cached in the network under the card id (same asOperatorVariableFacade, soLazyExpression-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;lastNetworkHashhandling mirrorsOperatorVariableFacade.ValueOperatorwrapping the newExpressionOperator(extendsOperatorBase). Inputs are the parameter types, output is inferred. Referenced cards are bound as variables when the card is built, likeCurriedOperatordoes, and a wrapper variable forwards their invalidation. Each call evaluates nodes lazily and at most once, without registering listeners, sochoiceonly evaluates the branch it needs.materialize()replaces refs by their current materialized values and keeps parameters. Materializing apipethat contains an expression operator also materializes it (reuses the Fix crash when displaying piped operators #1705 pattern inCombinedOperator).ExpressionOperator.Serializerwrites 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 newintegrateddynamics:expressionfacade. Old cards load and evaluate unchanged; loading an old card into the LP yields a one-level tree.Type inference (
ExpressionTypeChecker): walks the tree withgetInputTypes,validateTypesandgetConditionalOutputType, 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 foridContext.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:amountof 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 (where1.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 isconcat(see Text), and complete choices print asc ? a : b, so names never contain LP symbols such asNBT{}.get_integeror=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:andType: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)Lava (250 mB)).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.map,filter,reduce,apply, ...) every operator is listed. Picking one that cannot give an operator puts it in as a value, somap((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.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.+of the List element was in that corner, under the arrow button, so it moved left, next to the List element's type.+that reads+ Parameterwhile 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.?, highlights the part of a hovered slot, and can be clicked and edited (see Text).2 type error(s), the first in input 2 of *, or the message of an error of an operator as a whole (such aschoicebranches 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.Type an expression, such as (x: integer) => x * 2, or pick an operatoras 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+ Parameteradds a parameter to the text, asx =>in front of it or after its other parameters. The existing "Bind new Variable Card" arrow already points at the operator list.LogicProgrammerExpressionActionPacketcarrying small actions (const,op,param,clear,nav, parameter add/remove/rename/type), handled by the side-independentExpressionEditor. The server only accepts slot edits for the node being edited and ignores malformed actions.Text (
ExpressionParser,ExpressionPrinter.printExact)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.+ 3and pressing Escape within 750 ms lost it, while pressing it a moment later kept it.(x: integer) => x.add(3)staysx.add(3), also with its spaces, brackets and line breaks, and is not printed asx + 3. The editor keeps the text next to the tree, as do the history and the written card (textnext toexpression), 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.tokenizerecords 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).item.isStackable()hasitem,.,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.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 instrength (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 inItem.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 (chemforintegratedmekanism:chemicalstack). Parameters only list the types they can have, and values no categories.setaction in the same packet. The server deserializes it with the existing limits (depth 64, 8 parameters, known operators and types) and also checksExpressionEditor.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.(a + b) * 2while 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. Thesetaction does the same while no operator element is active, which is what the empty panel uses.(item: item) => item.isStackable() && item.size() >= 16from the IS book. Only syntax that means the same in both is taken over (see Known limitations).length(s)for strings,length(l)for lists), and otherwise the global one (stringLength). Both are accepted when parsing.item.size()can be written forsize(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 turnssize(item)intoitem.size(). The names and argument order are ID's, solist.filter(f)is a type error, asfiltertakes the operator first, unlike JavaScript's array method.+ - * / % == != < <= > >= && ||with C precedence, prefix!, andc ? a : b.===and!==are read as==and!=, which already compare strictly.+isconcatas soon as one of its inputs is a string, both when parsing and when printing.concatof inputs without a known type is printed asstringConcat(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).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 areitemandfluid, as the game calls them (itemstackandfluidstackare read too). The global names of their operators, such asitemstackSize, keep the ids, as scripts use them, and TODOs inValueObjectTypeItemStackandValueObjectTypeFluidStackmark 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)isapply(f, x),f()isapply0(f), up toapply3for three inputs, and it is printed withapply.true/false,1,5L,1.5/1e3/2d/NaN/Infinity, and strings in double or single quotes with\" \' \\ \n \t \r \uXXXXescapes, printed in double quotes.["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{a:1b},@nbt [1,2],@nbt 5, and@nbt()for no NBT.@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 theminecraftnamespace. The part after the type is the same as what IS gives foritem.id_item, as both use the value type's serializer.@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 asamount: 1000for an amount that is stored as1000L.#42refers 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#labelonly in that case, and#idotherwise.ExpressionPrinter.printExactis the inverse ofExpressionParser.parse, and the tests check this both ways.ExpressionParseris a recursive descent parser with a small lexer, for at most 8192 characters, with 31 translatable error messages inExpressionParseException.ExpressionEditor.parseadds 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 asa + b + c + ...stays linear.ExpressionTextField(client only) uses vanillaMultilineTextFieldas 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 inExpressionTextNavigation, 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) withOperatorLPElementRenderPattern.ExpressionPrinter.tokenizeExactgives 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.tokenizegives 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.ExpressionHistoryrecords changes by observing the expression and its text while rendering, so no action needs to know about it.ContainerScreenLogicProgrammerBaseforwards 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.true,NaNor1x.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.onPartActivatedhandled every wrench before the generic part code. A Wrench in a config mode now pastes; in the other modes it still rotates. Covered byGameTestsWrenchConfig.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_3is built fromITEMSTACK, so the fluidwith_dataoperator has the unique nameitemstack_withdataof 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 itemwith_dataoperator turns into the fluid one after a save and load. It is the only duplicate unique name. Fixing it changes which operator existingitemstack_withdatadata loads as, so I left it for a separate change.OperatorNamesuses global names to recognize registered operators, so it does not depend on this.Deviations from the plan, and why
logic_programmer.pngis untouched.#limit, not"limit". Quotes are used for string constants, so a quoted label would be ambiguous.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
+of the List element is right of its type instead of in the top right corner.Things to decide on
#123for 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#idon the server would be easy to add, but it would also reject loading and re-applying a card whose referenced cards are elsewhere.(a + b)while typing(a + b) * 2switches 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.setaction 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 + #2from 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.Known limitations
map(x => x * 2, l)cannot be written as such. It needs a second card, or partial application inside the slot, as inmap(apply((*), 2), l).:and@. Operator names, parameters and labels after#are not completed, and there is no key to open the list without typing.5,5L,1.5, and/of two integers gives an integer, so7 / 2is 3),-xonly works for number literals (0 - xworks), there are no statements,letoridContext.ops, untyped parameters can make shared names such assizeambiguous 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.#idreferences differ per world, so a shared expression with#12refers to whatever card has ID 12 in the other world, without a warning. Copy writes#labelwhen exactly one card has that label, and the book recommends labels for shared expressions.#5 + 1counts as Any there, and type errors involving it only show in the network.API additions for add-on authors
ValueTypeItemStackLPElement, ifcreateLogicProgrammerElementreturns 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(), andgetReferenceTypes()andgetText()with a default), handlerExpressionVariableFacadeHandlerwith type keyintegrateddynamics:expression.ReferencePlaceholderVariableFacadewith handlerintegrateddynamics: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(newIOperatorimplementation) with operator serializerintegrateddynamics:expression.ExpressionTrees(serialization, paths, queries),ExpressionTypeChecker(including the inferred type of every node, andInferencefor nodes built bottom-up),ExpressionPrinter(includingprintExact,printLiteralandtokenizeExact),ExpressionParser(includingcompleteandtokenize) withExpressionParseException,ExpressionTextNavigation,OperatorNames(includinggetMethods),ExpressionEvaluator.ExpressionEditor(includinggetText(), thesetaction with its text,isEditable,parse,isParameterTypeandgetParameterTypes),OperatorLPElement.getEditor()/applyExpressionAction(...)/getCurrentOperator()/findReferencedCards(Player)/setExpression(...)/getInexactItemConstant(slot),ValueTypeItemStackLPElement.getItemStackToValue(),ContainerLogicProgrammerBase.applyExpressionAction,refreshElementInventory,setExpressionSlotFilter, layout constantsEXPRESSION_PANEL_*,ContainerScreenLogicProgrammerBase.isExpressionPanelOpen(), andRenderPattern.getRenderSymbol().LabelsWorldStorage.getVariableIds(label).IOperatorVariableFacadeis 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 aLazyExpressionchain, invalidation, parameter binding,apply/filter/map/reduce/pipe/flip/currying, lazychoice, operator serialization, materialization including insidepipe, 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_2andflip, 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, withitemandfluidprinted 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 theLsuffix 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@listform; 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 as1.5read as1or300as44b; 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 throughitem.size(x) >= 16by 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) andTestExpressionTextField(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. NewGameTestsExpressions: ref + inline constants on a Display Panel, equality with the equivalent classic card chain,item => size(nbt(item)) > 1used by a classicfiltercard over an Inventory Reader list,applyof 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#addand#doubleare expression cards in the same network, three parameters partially applied throughapply, 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) * 2as 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 inTestExpressionValueLiteralsInGame(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; andwithInputOutput(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.item => size(nbt(item)) > 3from 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) andx => x > 1 ? x * 2 : x(x inferred as Number), and caused a branch type mismatch inchoiceto 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): builtfilter(#bignbt, #items)(written as a classic card) andlength(filter(#bignbt, #1))in a real network with an Inventory Reader, Variable Store and two Display Panels, reloaded the world (both panels kept[Sword]and1), used the Labeller, made an item constant in the Portable LP, and checked that the List and Recipe elements are unchanged. Builtx => getInteger(nbt(x), "key"), which showsitemstackNbtwhile the nested slot is empty andnbtoncexis 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).e245594print->, now=>, and the ones before71eeee7print calls such assize(item), nowitem.size(), and the ones beforeb0df4cbprintitemstackandfluidstack, nowitemandfluid, and the ones before39ef904show typed text printed once the text lost focus, which now stays as typed, and the ones beforeba5e4c0show the call at the cursor in the footer, which is now in a popup above the line):Integer -> String -> String), a wrapped type, a slot error, and the full message on hover+: the+adding an entry, and the panel's arrow button hiding the panely+1staying as typed while focused and printed as(x * y) + 1once it lost focus (before39ef904, it now stays as typed)+ 4applying it, in the panel of an expression and in the empty panelonClosecalled directly, as clientdevbridge's own close bypasses it(a: integer) => a + #5, which switched to Arithmetic Addition with the real#5card in its slot and rewrote the card+ 3to an applied(input: integer) => input + #N: with an Integer card#5typed, 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 gaveinput => (input + #N) + 3.(input: integer) => input +, the type once#5was added, Arithmetic Addition becoming active after the pause with the real#5card in its slot and the text still focused, continuing with+ 3, and a card written from(n: integer) => n + #5readingn => n + #5,Integer -> Integer@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 asl => l == ["apple", "pear"], and a misspelled item shown asNot a valid itemstack value, it would be read as {}instead of turning into an empty slot as it did before(item: itemstack) => item.isStackable() && item.size() >= 16(Logical And,Item -> Boolean, printed asisStackable(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 asapply(f, x) == 1)amount(@fluidstack{amount:1000,id:"minecraft:water"}), and a typed 250 mB of lava shown as a lava bucket, whose tooltip readsLava BucketandLava (250 mB)(item: itemstack) => item.isStackable() && (item.size() >= 16)typed: the outer parentheses highlighted with the cursor after the last one, a double click onisStackableselecting only that name, a third click selecting everything, and a double click onitemin(item.size()selecting onlyitem. The clicks were sent to the screen within one script, as clientdevbridge's separate clicks are further apart than a double click.item.listing the methods of an item over the name bar of the element,sileavingsize, Enter insertingsize(), Tab insertingstrength(with the footer showingItem.strength(Block): Double, a click insertingcanHarvest(, the mouse wheel moving toencode, 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(x: intlistinginteger Integer, Enter insertinginteger,, i: itelistingitemstack Item,@iteafter!=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"}), keepingi.size()(x:the types with their names in their colors (Boolean blue, Double yellow),itemandfluidunder their new names,ilistingingredients,integeranditem, 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+ Parameterwith the text box below them,+ Parametertwice giving(x, y) =>with the cursor at the end, Undo emptying it, and(x: item) => x.fluidStack().block().breakSound()applied without the text moving; clickingfluidStackgiving the breadcrumbbreakSound › … › fluidStack; and clicks across the 4 pixels around the boundary between the brackets ofx.fluidStack()placing the cursor between them, with the footer showingItem.fluidStack(): Fluid(only a click on the closing bracket did before)(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 + 3and disabling itself, and Undo bringing backx.add(3);(x: item) => ((x.size()) > 16) && !x.isStackable()keeping its brackets and spaces, with&&in the color of the open operator, a click onsizeopening 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(x: item) => x.strength(, while the footer showed the parse error, withBlockunderlined; above the second line of(x: item) => x.strength(@block{Name:"stone"}) > 1.5 && x.size() >= 16with the cursor afterstrength(, covering the first line, and above the third with the cursor inx.size(), showingItem.size(): Integer, while the footer kept showingItem -> Boolean; and hidden while the list afterx.inside the call was shown, coming back once Escape closed the lista => a + #5asInteger -> Integer(it saidInteger -> Anybefore the reference types were used), and#5 + 1asIntegerlength,concat) and string literalsScreen.hasControlDown(),Screen.hasShiftDown()andScreen.hasAltDown(), so I used the buttons instead, and the keys are covered by the unit tests ofExpressionTextNavigation. The macOS keys could not run in game at all, since the dev client runs on Linux. What pasting does to text (it goes throughStringUtil.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.amountworked, but a parameter typedintegratedmekanism:chemicalstackdid not parse,amount: 1000was 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 namechemicalstackAmount(c)),(c: chemicalstack) => c.amount() > @chemicalstack{id: "mekanism:hydrogen", amount: 1000}.amount()printed with namespaces and1000L, 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.item => length(name(item)) > #thresholdin 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'snameof 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