Repository navigation
Conversation
|
Tested this branch (b83d2e9) with One workbook pulls colour codes from a linked workbook, e.g. The files can't be shared, but I'm happy to rerun the corpus on later revisions. |
|
Thanks for running this. Linked sheet names with Cyrillic and a trailing space were exactly the case I couldn't test locally, so 1,980 → 0 with no other workbook changing is really useful. |
2ca4cd3 to
44d0e30
Compare
|
Rechecked the rebased head |
|
Tested One problem: a typed
Main refuses both ("There's a problem with this formula: unexpected Sheet("Sheet1", None)"), so this is new with this PR. The formula bar shows linked cells as Suggested fix: on entry, refuse a Smaller: #829 builds on this branch and does the same at |
…with the link A formula reading another workbook ([1]Prices!B2, '[2]P&L'!A1:B9, a name over such a range) recalculated to #REF!: the externalLink parts were passed through on save but never read. Excel calculates these from the cached copy of the linked cells it saves in the file, as it does whenever the source is closed. On a 4,224-file real-world corpus this was the largest root cause of calculation mismatches. The reader now loads each link's sheet names, cached cells, refresh-error flags and defined names into Workbook::external_books; the lexer reads unquoted [1]Sheet!A1 and [1]!Name; references, linked names and [0]!Name evaluate from that data; the *IF(S) range arguments refuse linked-workbook values and written errors, as Excel does with a closed source; and linked sheets print unquoted when the sheet name allows, so saved formulas keep their text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… prints back References into a linked workbook became value arrays when evaluated, so `@` and implicit intersection took the top-left value (`=@[1]Data!B:B` in row 4 gave the header), ROWS/COLUMNS/INDEX saw the block trimmed to the last cached cell (`ROWS([1]Data!A:A)` = 3), and every lookup rebuilt the whole block. They are now Areas whose sheet number (from LINKED_SHEETS up) stands for the linked workbook and sheet; cells are read from the saved values only when the reference is dereferenced (Evaluator::cell, Evaluator::block_values), and a linked sheet's extent is remembered per pass like a local used range. The printer dropped the quotes from '[1]Jan:Dec'!A1 and the lexer couldn't read [1]Jan:Dec!A1. The lexer now accepts the unquoted MS-XLSX sheet-range form, and the printer quotes a range when either sheet name needs it.
…]Jan:Dec
OFFSET took a reference into a linked workbook as its base and read the
saved link values. Excel errors there while the source is closed: the Enron
West NatGas Prices cells wrap OFFSET('[21]Curve Summary'!...) in ISERROR and
cached the backup value. OFFSET now answers #VALUE! for a linked base, the
answer of the other reference-only parameters given a closed workbook's values.
Renaming a local sheet rewrote the second end of [1]Jan:Dec!A1, which names a
sheet of the linked workbook; rename_sheet and delete_sheet now skip spans
whose first end is in a linked workbook.
Workbook::external_books is behind an Arc, like sheets and preserved parts,
so each edit's workbook clone and the undo history share the link cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…[0] is this workbook
44d0e30 to
36befbd
Compare
…ixes; [0] names stored local; CF/DV refuse other workbooks; paste into another workbook drops [n]
…New Window of the same workbook)
|
Thanks, that was a real regression: I could only check Excel's side through your report, and a file Excel refuses to open is the worst outcome. Fixed in
Tests: Since #829 builds on this branch, it gets the check from here. |
Main now quotes the sheet part of a sheet-qualified name, so after the rebase a name of a linked workbook printed as '[1]'!Rate, in the formula bar and in saved files, where it names a sheet called [1] in this workbook. Excel writes [1]!Rate. Same change as in storytold#1200. Co-authored-by: Herr-Enklo <168219178+Herr-Enklo@users.noreply.github.com>
…rs survive whole columns, legacy cells stay legacy
Typed =[1]!A1, =[1]!$B$2, =SUM([1]!A1:B2) and =[0]!A1 were accepted and saved as a
reference to a sheet called [1] or [0] ('[1]'!A1), which no sheet can be. Only a defined
name can follow a bare [n]!, so a cell reference there is now refused on entry in cells,
defined names, conditional formats and data validation. Read from a file, the same text
prints back as written ([1]!A1), never quoted.
[0] is this workbook. The parser now reads [0]Sheet1!A1 as Sheet1!A1 and [0]!Rate as Rate,
so formulas read from files are local references that follow inserted rows and sheet
renames and deletions; before, adjusting left [0]Data!B2 alone while evaluation read Data.
The special cases for [0] in entry, sheet lookup and name evaluation are gone.
Sheets whose last link refresh failed are #REF! outside their saved cells, but trimming a
large reference to those cells dropped the errors: [1]Data!A:A summed to 5 where
[1]Data!A1:A2 was #REF!. Trimming now keeps one row and column past the saved cells on such
sheets, so any reference reaching an uncached cell is #REF!.
The link cache was saved in GridCraft JSON while the link parts were not, so a JSON round
trip kept the values and a typed [1] was then saved into a file with no externalLink part.
The cache is no longer serialised; reopened, the workbook has no links and [1] is refused.
Legacy cells holding linked ranges (<f>[1]Data!$B:$B</f>) were promoted to dynamic arrays
on save. Linked ranges no longer make a formula dynamic, so they save as legacy formulas as
on main. [1]Data!#REF! was read as a plain #REF! and lost its link on save; it is now kept
(as a name that evaluates to #REF!) and printed with its link.
Data-bar formula thresholds now get the same refusal of other workbooks as the rules'
formulas.
…ssion, not a name [1]Data!#REF! was carried as a defined name called "[1]Data!#REF!" so that it printed back with its link. Anything that looks up or walks defined names (name evaluation, the entry checks, name lists) could take it for one. It is now Expr::LinkedRefError holding the sheet it was in: it evaluates to #REF!, prints and saves as [1]Data!#REF! (or [1]Jan:Dec!#REF!), and the entry checks read its sheet like a reference's, so [1]!#REF! and an unlinked index are still refused.
…ll's saved value A cell such as =[4]!Formula_Name, where the link saved the name as refersTo="#REF!", or =[2]!HelloWorld, where it saved no definition at all, now showed #REF! instead of the value Excel saved for it. Linked values only change when the link is updated, so Excel keeps showing that value. Names saved without refersTo were also dropped on read, which made them look undefined. The reader now keeps such names with an empty definition. When a formula uses a linked name whose definition can't be read from the link's saved data (no definition, #REF!, a sheet range, a sheetless reference, a table, or sheets and names the link didn't save), the evaluator tells the host, and the recalculation keeps the cell's saved value (a spill or legacy array range whole). A name the link doesn't define, a refresh-error sheet's unsaved cells, and a formula with no saved value are still #REF!.
A formula that reads another workbook (
=[1]Prices!B2,=SUM('[2]P&L Prices'!B1:B9),=VLOOKUP(A7,Gas,4,FALSE)where the nameGasis[1]Bogey!$A$3:$B$33) recalculated to#REF!in GridCraft.#NAME?showed up in a few cases too. Excel calculates these from the copy of the linked cells it saves in the file (xl/externalLinks/externalLinkN.xml,sheetDataSet), as it does whenever the source workbook is closed. The pass-through from #330 kept those parts on save, but nothing read them.This was the largest root cause in a new corpus of 4,224 real-world workbooks. Over the Excel-saved files it accounted for about 21,500 mismatching cells directly (
[n]in the formula) and most of another ~11,000 counted under VLOOKUP/MONTH, through names that point into linked workbooks.What changed
Workbook::external_books: Arc<Vec<ExternalBook>>, where[n]in a formula isexternal_books[n-1]. Each book holds its sheets with their cached cells, arefresh_errorflag per sheet, and the linked workbook's defined names (ExternalName). The cache is behind anArc, like sheets and preserved parts, so the workbook clone each edit makes and the undo history share it instead of copying it (the largest corpus file has 157,198 cached link cells). It is only set when a file is read, and it isn't saved in GridCraft JSON, which doesn't carry the link parts either: a JSON copy has no links, so a typed[1]is refused there rather than saved into an.xlsxwith no link for it.crates/xlsx/src/external.rs, new). Follows<externalReferences>inworkbook.xmlto eachexternalLinkpart and readssheetNames,sheetDataSet(numbers, strings, booleans, errors) anddefinedNames. A missing or unreadable part leaves an empty book, and references into it stay#REF!.[1]Prices!A1,[1]!Nameand the sheet range[1]Jan:Dec!A1(MS-XLSXsheet-range = [workbook-index] sheet-name ":" sheet-name) as sheets in a linked workbook (quoted'[1]P&L'!A1already lexed). The printer writes[1]Prices!A1and[1]Jan:Dec!A1unquoted when the sheet names allow it, as Excel does, so saved formulas keep their original text. A sheet range is quoted when either sheet name needs it ('[1]Jan:My Dec'!A1). References, including these sheet ranges, print as text that parses back to the same formula. So do sheet-qualified names:'[1]My Sheet'!Nameprints quoted, and a linked workbook's own name[1]!Rateunquoted.[1]Data!#REF!, a reference Excel found deleted in the linked workbook, keeps its link when printed and saved (it was saved as a bare#REF!). It is its own expression node (Expr::LinkedRefError, the sheet it was in), which evaluates to#REF!, so nothing that looks up defined names mistakes it for one. A cell reference after a bare[1]!, where only a defined name belongs, prints back from a file as written ([1]!A1), never as'[1]'!A1, a sheet called[1].[1]Jan:Dec!A1) names that workbook's sheets at both ends. Renaming or deleting this workbook'sDecleaves it alone (adjust::rename_sheet,adjust::delete_sheet), whileDec!A1is renamed or becomes#REF!as before.crates/calc/src/eval.rs).Areawhose sheet number, fromLINKED_SHEETSup, stands for the linked workbook and its sheet). Its cells are read from the cached values only when the reference is dereferenced (Evaluator::cell,Evaluator::block_values). So@and implicit intersection take the cell on the formula's row or column (=@[1]Data!B:Bin row 4 is[1]Data!B4), and ROWS, COLUMNS and INDEX see the size written:ROWS([1]Data!A:A)is 1048576 andINDEX([1]Data!A:A,10)past the last cached row is an empty cell. Reading values trims whole columns to the last cached cell, as references into this workbook trim to the used range. On a sheet whose last refresh failed it keeps one row and column past the cached cells, so the cells it doesn't hold still give#REF!(below).#REF!: INDIRECT reads another workbook only while it is open.#VALUE!. The corpus shows Excel errors there but not which error: the EnronWest NatGas Prices 1031.xlsxcells PowerPrices!D9:D18 wrapOFFSET('[21]Curve Summary'!$D$6,2,0,25,1)inIF(ISERROR(...),backup,...), and Excel cached the backup in every cell even though the link cache holds every cell OFFSET would read. I picked#VALUE!because a closed workbook hands a function values rather than a reference, and a reference-only parameter given values answers#VALUE!. That is the answer the corpus pins for the *IF(S) range parameters above. The choice is unverified: no source or cached cell shows OFFSET's own error. ISREF, AREAS, ROWS/COLUMNS, INDEX and SHEET take linked references as before. FORMULATEXT gives#N/Aand ISFORMULA FALSE, because no formulas are saved for a linked workbook.#REF!.refreshError="1") keeps the cells it had saved. Any other cell in it is#REF!, alone or inside a block (=SUM([2]Failed!A1:B1)is#REF!), and a whole column agrees with the block inside it (=SUM([2]Failed!A:A)is#REF!, likeA1:A2). This matches the cached results in the Enron files that have such sheets.[1]!Nameand[1]Sheet!Nameevaluate the linked workbook's name over its cached values. Names used inside that name are also looked up in the linked workbook, sheet scope first. A name the link doesn't define is#REF!. A name whose definition can't be calculated from the saved data (saved as#REF!or with no definition, as for DDE/OLE links, or holding a sheet rangeJan:Dec!B2, a sheetless reference in a workbook-level name, a table reference, or sheets and names the link didn't save) never reads this workbook's cells: the cell keeps the value Excel saved for it (a spill or legacy array range whole), since linked values only change when the link is updated. With no saved value it is#REF!. The reader keeps names saved withoutrefersTofor this.[0]is the workbook itself:[0]!Nameis its name,[0]Sheet1!A1itsSheet1!A1. The parser reads[0]Sheet1!A1asSheet1!A1and[0]!NameasName, so a formula read from a file is a plain local reference: inserting rows, renaming or deleting the sheet adjust it like any other, and it saves asSheet1!A1.#VALUE!(or the cell's own error), and a written error such as#REF!left by a deleted range, which gives that error. Excel answers this way when the source workbook is closed, and the corpus caches show it, for exampleSUMIF('[4]Statements'!$A$5:$A$1305,...)=#VALUE!andSUMIF(#REF!,">0")=#REF!. Arguments from anywhere else are left alone, so MAP/BYROW/REDUCE LAMBDA parameters still work there (=BYROW(A1:C3,LAMBDA(r,COUNTIF(r,">0")))).Measured
gridcraft-cli checkon each of the 4,224 files (one process per file, 60 s timeout), with release builds of main (84acf62) and this branch (8aa525d):85 files have fewer mismatches. 2 files have more, 12 cells in total:
external-ref-wholecol-xlookup.xlsx(+11):=[1]Data!B:Band=XLOOKUP([1]Data!$J:$J,…)in single legacy cells now return the column and spill. Excel applies implicit intersection there, which is the implicit-intersection PR's area (XLSX: legacy formulas (no dynamic-arraycm) open with Excel's implicit@and calculate like Excel #417). Saving writes these cells back as the legacy formulas they were, as main does: a range into a linked workbook doesn't turn a legacy cell into a dynamic array. Test-merged with XLSX: legacy formulas (no dynamic-arraycm) open with Excel's implicit@and calculate like Excel #417, those 10 cells read with the implied@like local ranges, match Excel's cached 10..14 and save as legacy<f>withoutcm; the file is left with one mismatch, B8, a dynamic-arrayXLOOKUPover whole linked columns that gives#SPILL!where Excel cached#VALUE!(main showed the cached value because it didn't calculate the formula). The intersection itself works on references into linked workbooks: with an explicit@,=@[1]Data!B:Bin D2..D6 gives 10..14, the values Excel cached in those cells.tdf85617.xlsx(+1): the cache says#N/Awhere the linked values give4.5. Written by LibreOffice tests; probably a stale cache.The number of formulas checked dropped from 1,429,022 to 1,427,745. Formulas skipped as volatile rose by 6,376 and no-oracle formulas fell by 5,099. My guess, not traced: formulas that used to stop at
#REF!now evaluate far enough to reach a volatile call.Earlier rounds, measured on b83d2e9 and its follow-ups: after the first review fixes (references into linked workbooks kept as references, sheet ranges), on the 276 corpus files that have external links: 20,030 mismatching formulas before, 20,029 after. The one change is
CORE!P18=A2:[2]MISSING!A2informulas/test__test_files__test.xlsx, now#VALUE!as cached instead of#REF!. No formula newly mismatches among the onescheckcompares, and the round-trip result is unchanged (6 of 276 files differ, as before).checkskips OFFSET formulas as volatile, so it couldn't see that 2133e84 let OFFSET read linked values. The second review fixes that: with a release build, the West NatGas cells PowerPrices!D9:D15 and D18 now evaluate to Excel's cached backups (39, 44.25, 44, 39, 39, 37, 38, 59.8). 2133e84 gave 37, 37.5, 36.5, 21.44, 35.25, 34, 35, 57. The 11 report files indist/web/filesgive the samecheckoutput as at 2133e84 (2 mismatching formulas out of 261,528, 0 round-trip differences).Speed,
gridcraft-cli check, release builds of b83d2e9 and this branch on the same machine: 3,000VLOOKUP(An,[1]Data!A:B,2,FALSE)over a 20,000-row linked table took 5.6 s before and 0.7 s after (the same lookups over a localData!A:Btake 1.6 s on both).mike_grigsby__27684__PG&E.xlsxwent from 0.80 s to 0.27 s.mike_grigsby__27922__HS_WESTminusPA.xlsxtakes 18.7 s on both. Blocks in a linked sheet are now built from its saved cells in one pass over the rows they cover (ExternalSheet::cells_in), and the extent of a linked sheet is remembered for the recalculation pass like the used range of a local sheet (used_range_of,PassHost::used_range).Verification
references_into_linked_workbooks_read_the_saved_values(crates/calc/src/tests.rs, new) covers cells, blocks, names over linked ranges, missing books and sheets, refresh-error sheets (single cells and blocks),[0]!, linked names (nested names, sheet ranges, sheetless references), SUMIF/COUNTIF on linked values and on#REF!, and printing. It fails on 95c8776, where every reference is#REF!.if_family_ranges_inside_lambdas_stay_references(new): BYROW+COUNTIF and MAP+SUMIF over a range of this workbook give 2, 2 and 1, 0. It guards against the reference check rejecting LAMBDA parameters.unmodelled_parts_survive_a_round_trip(crates/xlsx/src/tests/passthrough_tests.rs) now also asserts the link's cached value is read. The formulas[1]Sheet1!$A$1andSUM([1]Sheet1!A1:A3)*2are still written exactly as read.references_into_linked_workbooks_keep_their_position_and_size(crates/calc/src/tests.rs, new):=@[1]Data!B:Bin D2..D6 gives 10..14,=@[1]Data!B3:B6gives 12 in row 4 and#VALUE!in row 9,=@[1]Data!A1:B1in column B givesPrice;ROWS([1]Data!A:A)= 1048576,ROWS([1]Data!A1:A100000)= 100000,COLUMNS([1]Data!1:1)= 16384,INDEX([1]Data!A:A,10)is an empty cell; SUM and VLOOKUP over whole linked columns;ISREF([1]Data!B2)is TRUE;INDIRECT("[1]Data!B2")is#REF!. It fails on b83d2e9 (@gave the header,Price).linked_workbook_sheet_ranges_round_trip(crates/formula/src/tests.rs, new):SUM([1]Jan:Dec!A1),SUM('[1]Jan:Dec'!A1),SUM('[1]Jan:My Dec'!A1),SUM('Jan:[1]Dec'!A1)and[2]Prices!B2parse to the expected sheets, and what the printer writes parses back to the same formula. It fails on b83d2e9 (the unquoted form didn't parse, and the quoted form printed unquoted).offset_refuses_a_base_in_a_linked_workbook(crates/calc/src/tests.rs, new):OFFSET([1]Curve!$D$6,2,0,3,1)is#VALUE!though the link cache holds D8:D10, and the West NatGasIF(ISERROR(AVERAGE(OFFSET(...))),[1]Backup!$D$38,0)gives the backup, 39. OFFSET on a local base still works. It fails on 2133e84.renaming_or_deleting_a_sheet_leaves_linked_spans_alone(crates/formula/src/tests.rs, new): renaming localDectoDecemberturnsSUM([1]Jan:Dec!A1)+Dec!A1(unquoted and quoted) intoSUM([1]Jan:Dec!A1)+December!A1, and deleting it givesSUM([1]Jan:Dec!A1)+#REF!. The rename fails on 2133e84 ([1]Jan:December).names_of_linked_workbooks_print_as_excel_writes_them(crates/formula/src/tests.rs, new):[1]!RateandSUM([1]!Rate,1)print unquoted, as Excel writes them;[1]Prices!Rateand'My Sheet'!Totalare unchanged. After the rebase onto 84acf62, main's quoting of sheet-qualified names printed'[1]'!Rate(in the formula bar and in saved files, where it names a sheet called[1]); the test fails on 80a583c. Same printer change as in Workbook Links: update linked workbooks from their files, change source, break links, startup prompt (builds on #464 and #829) #1200, so that PR rebases cleanly.book_only_references_print_back_without_quotes,deleted_references_into_linked_workbooks_keep_the_linkandworkbook_zero_is_this_workbook(crates/formula/src/tests.rs, new):[1]!A1andSUM([1]!A1:B2)print as written,[1]Data!#REF!+1andSUM([1]Jan:Dec!#REF!)keep their link whileData!#REF!stays#REF!, and[0]Data!B2,'[0]My Data'!B2,[0]Jan:Dec,[0]!Rateparse to the local forms and follow a sheet rename. They fail on 784a585^ ('[1]'!A1,#REF!+1,[0]Data).formulas_into_linked_workbooks_save_back_as_written(crates/xlsx/src/tests/roundtrip.rs, new): legacy<f>[1]Data!$B:$B</f>and[1]Data!$B$2:$B$4*2save as legacy cells (nocm, not="array"),[1]Data!#REF!+1,[1]!A1andSUM('[1]My Data'!B2:B4)save as read, and[0]Data!B2saves asData!B2. It fails on 784a585^.references_into_linked_workbooks_read_the_saved_valuesnow also checksSUM([2]Failed!A:A)andSUM([2]Failed!A:Z)are#REF!likeA1:A2(5 on 784a585^), and that[2]Rates!#REF!is#REF!.linked_names_that_cannot_be_calculated_keep_their_saved_value(crates/calc/src/tests.rs, new):[1]!Broken(#REF!),[1]!Blank(no definition) and a name over a sheet range keep their saved values through recalculations, while a name the link doesn't define and a formula with no saved value are#REF!. It fails on 9c68486.names_saved_without_a_definition_are_kept(crates/xlsx/src/external.rs, new) reads<definedName name="HelloWorld"/>as a name with an empty definition. Over the corpus this fixes=[4]!Formula_Namein EPPlusExternalReferences/dde.xlsxand=[2]!HelloWorldinempty_ext_defined_name.xlsx.cargo fmt,cargo clippy --all-targets -- -D warningsandcargo testfor gridcraft-formula, gridcraft-model, gridcraft-calc, gridcraft-engine and gridcraft-xlsx. A test merge with XLSX: legacy formulas (no dynamic-arraycm) open with Excel's implicit@and calculate like Excel #417 (8f9d588) conflicts only incrates/xlsx/src/array_formula.rsand the file-format-parity header and revision rows. Resolve by taking XLSX: legacy formulas (no dynamic-arraycm) open with Excel's implicit@and calculate like Excel #417's side ofarray_formula.rs(its legacy analysis replaces the scan this PR changed, and it treats a linked range like a local one) and keeping both revision rows. Merged that way, legacy<f>[1]Data!$B:$B</f>in row 4 reads as@[1]Data!$B:$Band gives 12,[1]Data!$B$2:$B$4*2in row 5 gives#VALUE!, and both save back as legacy<f>withoutcm; the merged formula, calc, engine and xlsx tests pass.cargo xtask ci: all 8 steps passed on ca519cf (rebased on 84acf62).cargo xtask ci: all 8 steps passed on b83d2e9. For the review fixes:cargo fmt --all,cargo clippy --all-targets -- -D warningsandcargo testfor gridcraft-formula, gridcraft-model, gridcraft-calc, gridcraft-engine and gridcraft-xlsx, andcargo check --workspace --all-targets.Typed
[n]that isn't a linkReported by @iluebbe (Excel 365): the lexer change let a typed
=[2]Sheet1!A1in a workbook with one link (or=[1]Sheet1!A1in one with none) enter and be saved as typed, and Excel 365 then can't open the file at all, even with repair. Before this PR the lexer rejected[n]on entry. Now formula entry checks it:check_linked_booksincrates/engine/src/cmd/edit.rs, called frominput_to_cell, the one place user text becomes a cell formula (cell.setand so the formula bar,range.setValues,range.fill, array entry, Text to Columns, Replace, paste of formula text, What-If), and fromformulas.defineName. An index that isn't one ofWorkbook::external_booksrefuses the formula with "There's a problem with this formula: [2] isn't a linked workbook." and leaves the cell unchanged, as an unparsable formula did before (paste and Replace keep such text as text, as before). Formulas read from files aren't checked, so they still load.=[0]Sheet1!A1was#REF!on this branch; Excel 365 reads[0]as this workbook. It is now entered as the plain local reference (=Sheet1!A1,[0]!RatesasRates), and a[0]Sheet1read from a file is read as the local reference.Only a defined name can follow a bare
[n]!([1]!Rates). A cell reference there (=[1]!A1,=[1]!$B$2,=SUM([1]!A1:B2),=[0]!A1) would be to a sheet called[1], and is refused on every route above, with CF and DV formulas too. Main refused these in the lexer.Review follow-ups, same commit series: either end of a sheet range is checked (
='Sheet1:[5]Dec'!A1is refused), and so are bracketed prefixes that aren't an index ('[-1]Sheet1','[Book2.xlsx]Sheet1': a sheet name can't contain[). A[0]defined name is stored as the local reference, so it follows a sheet rename. Conditional-format and data-validation formulas, data-bar formula thresholds included, refuse references to other workbooks, as Excel's dialogs do. A formula pasted into another workbook keeps a[n]reference only where the target holds the same link at that index (same linked sheets, saved values and names; a View ▸ New Window of the workbook shares them, so pasting between its windows keeps[1]S!A1live); any other[n]becomes#REF!, since the target doesn't have that link (Excel would copy the link over; GridCraft can't create links yet).Tests:
typed_references_to_workbooks_that_are_not_linked_are_refused_on_entry(crates/engine/src/tests.rs, new) refuses[1]…in a workbook with no links (cells and a defined name) and[2]…with one link, keeps the cell, accepts[1]with one link, enters[0]references and names as local ones with local values, and covers the follow-ups above (sheet-range ends, non-index prefixes, CF/DV and data-bar thresholds,[0]names across a rename, paste into a New Window and into other workbooks), refuses[n]!followed by a cell reference, and checks a JSON round trip comes back with no links.references_into_linked_workbooks_read_the_saved_valuesnow also reads[0]Sheet1!A1and[0]Sheet1:Sheet1!A1from formula text. Both fail without the fix.Not done
externalLinkparts are passed through unchanged..xls(BIFF8) external references (SUPBOOK/EXTERNNAME/CRN records) are still kept as values.[4]!Formula_Name, whose name the link saved as#REF!) aren't followed: the cell keeps the value Excel saved for it.SUM([1]Jan:Dec!A1)) now reads and saves correctly but still evaluates to#REF!.