Skip to content

perf: three-engine batch column access API + column-major ColumnFrame hot paths - #155

Merged
Liam0205 merged 10 commits into
masterfrom
feat/column-store-batch-access
Jul 7, 2026
Merged

perf: three-engine batch column access API + column-major ColumnFrame hot paths#155
Liam0205 merged 10 commits into
masterfrom
feat/column-store-batch-access

Conversation

@Liam0205

@Liam0205 Liam0205 commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

Answers a long-standing question — why does column storage show no significant advantage over row storage? — then fixes the root causes on all three engines.

Root causes (investigated on pine-go, recorded in llmdoc)

  1. Per-element Item() interface tax (first-principles cause): every operator read pays RLock + column map lookup per element — 7,210ns vs 262ns for an ideal hoisted column scan (27x). This flattens both storage layouts to the same order of magnitude before columnar layout can matter.
  2. Row-major code over column storage: three ColumnFrame hot paths (BuildInput validation, ItemWrites, Additions) iterated item-major with per-item map lookups.
  3. Workload shape: recall/removals/reorder pipelines are inherently row-friendly (zero-copy additions), and the calibrated production fixture is N≈10.

Changes

  • pine-go (fbf2ef7): column-major fixes for the three hot paths (first-error priority kept byte-identical) + types.ColumnReader optional interface + OperatorInput.ItemColumn(field) batch access (zero-copy view on ColumnFrame; defaults force a copy; per-element fallback; window-aware for data_parallel shards) + 10 built-in operator hot loops rewritten
  • pine-java (bf7ce0b): same pattern — Frame.itemColumnView default method + OperatorInput.itemColumn + same operator set
  • pine-cpp (95a3000): Frame::item_column virtual + OperatorInput::item_column + same operator set (read side only — C++ validation/write paths already had batch shapes). Also fixes a latent dangling-pointer bug in test_remote_pineapple.cpp (stack-local InputFieldSpec outlived by OperatorInput, surfaced as SIGSEGV by the new field-name compare, pinned via ASan) and a missing <memory> include in arena.hpp
  • fuzz (16440e8): new defaults+nil co-occurrence dimension — coverage showed the ItemColumn defaults-copy branch at 0 hits before; now exercised (defaults_nil summary stat added)
  • build (d414d89): macOS homebrew LuaJIT search hints for local cross-validate
  • docs: llmdoc investigation reflection + landing record, batch access API contract in operator-contract.md, roadmap step-1 note, CLAUDE.md language conventions rewrite

Results (Apple M5 Pro)

Benchmark row column before column after
BuildInput micro (1000×10) 34.8μs 53μs (48% slower) 4.9μs (~7x faster)
e2e transform-heavy 5000 items 5.68ms ~parity 3.56ms (~37% faster, -40% bytes)
e2e recall/filter/sort shapes ahead 40-90% gap gap narrowed to 10-20%

Semantics: element i of the batch API is identical to Item(i, field) including item-default substitution; returned views are read-only and Execute-scoped (safety via DAG hazard ordering).

Verification

  • pine-go: full test suite + race + golangci-lint 0 issues
  • pine-java: 251 tests green + checkstyle clean
  • pine-cpp: 219 doctest cases / 110k assertions green incl. ASan build
  • cross-validate sections 1-5, 9, 14 green (column-store parity 95/95 Go vs Java vs C++)
  • three-engine differential fuzz 120 rounds, 0 divergence, with function-level coverage confirming the new paths are exercised

Liam0205 added 9 commits July 7, 2026 14:34
Answers the long-standing question of why ColumnFrame shows no
significant end-to-end advantage over RowFrame in pine-go:

- Root cause 1 (first-principles): per-element OperatorInput.Item()
  interface tax (RLock + map lookup per element, 27x over an ideal
  hoisted column scan) flattens both storage layouts to the same
  order of magnitude.
- Root cause 2: three ColumnFrame hot paths run row-major code over
  column storage (BuildInput validation, ItemWrites, Additions).
- Root cause 3: typical pipeline shapes (recall/removals/reorder,
  small calibrated N) are inherently row-friendly.

Records prototype A/B data (column-major fixes: BuildInput 53us ->
3.9us, transform-heavy e2e +30% for column), the layered solution
path (column-major fixes -> batched column access API as the gate ->
typed columns + arena), and updates perf-evolution-roadmap step 1
with the "typed columns need a batched access API to pay off" data
point. Code changes remain an uncommitted prototype.
…access API

Root-caused why column storage showed no advantage over row storage
(see llmdoc/memory/reflections/column-vs-row-parity-investigation.md):
the per-element OperatorInput.Item() interface tax (RLock + map lookup
per element, 27x over an ideal column scan) flattened both layouts,
and three ColumnFrame hot paths ran row-major code over column storage.

ColumnFrame fixes (behavior-preserving, first-error priority kept
byte-identical):
- BuildInput strict/nullable validation: hoist column resolution out
  of the per-item loop (was one map lookup per item x field)
- ApplyOutput item writes: cache the last-resolved column across
  field-major write runs
- ApplyOutput additions: column-major two-pass batch append (was
  O(added x cols) map iterations)

New batch access API:
- types.ColumnReader optional interface + OperatorInput.ItemColumn:
  one lock + one lookup per column; zero-copy view on ColumnFrame
  (defaults force a copy), single-lock gather on RowFrame,
  per-element fallback for other FrameReader impls; window-aware for
  data_parallel shards; Item()-identical semantics incl. defaults
- Built-in operator hot loops rewritten to ItemColumn: normalize,
  sort, shuffle, dedup, condition, resource_lookup, copy, observe_log,
  remote_pineapple, lua (common-mode gather + item-mode hoist)

Benchmarks (Apple M5 Pro, 1000 items x 10 fields):
- BuildInput/column 53us -> 4.9us (was 48% slower than row, now ~7x faster)
- Additions/column 600us -> 352us
- e2e transform-heavy 5000 items: column ~37% faster than row
  (3.56ms vs 5.68ms, -40% bytes, -20% allocs); was at parity before
- recall/filter/sort shapes: row still ahead (inherent zero-copy
  additions + removals/reorder advantage), gap narrowed

New tests: ItemColumn semantics (row/column/materialized/defaults/
shard-window/absent-field) + storage A/B benchmark suite.
…h access API

Port of the pine-go column-store optimization (fbf2ef7) with identical
semantics:

- ColumnFrame.buildInput: hoist column/presence resolution out of the
  per-item strict/nullable validation loop; iteration stays item-major
  so first-error priority is byte-identical
- ColumnFrame.applyOutput item writes: cache the last-resolved column
  across field runs
- Frame.itemColumnView default method (optional batch read, null =
  fall back) + ColumnFrame zero-copy view + DataFrame single-lock
  gather
- OperatorInput.itemColumn: element i identical to item(i, field),
  incl. item-default substitution; zero-copy when the ColumnFrame has
  no defaults for the field
- Operator hot loops rewritten to itemColumn: TransformNormalize,
  ReorderSort, ReorderShuffle, MergeDedup, FilterCondition,
  TransformResourceLookup, TransformCopy, ObserveLog,
  TransformRemotePineapple, TransformByLua (common-mode gather +
  item-mode column hoist)

Tests: new ItemColumnTest (row/column parity, defaults, materialized,
window, absent field); full suite 251 tests green; checkstyle clean.
Port of the pine-go column-store optimization (fbf2ef7) with identical
semantics:

- Frame::item_column(field) virtual: all values of a field gathered
  under a single lock acquisition (vs per-element item() = lock +
  column lookup each). ColumnFrame walks the contiguous typed column;
  RowFrame gathers from per-row maps; window views translate offsets.
- OperatorInput::item_column: element i identical to item(i, field),
  including item-default substitution for nil slots.
- Operator hot loops rewritten: transform_normalize, reorder_sort,
  reorder_shuffle_by_salt, merge_dedup, filter_condition,
  transform_resource_lookup, transform_copy, observe_log,
  transform_remote_pineapple, transform_by_lua (common-mode gather +
  item-mode column hoist).

Unlike pine-go/pine-java, the BuildInput validation and item-write
paths already had batch shapes (validate_strict_items bitmap scan,
write_item_field_locked), so only the read-side API was ported.

Also:
- arena.hpp: add missing <memory> include (std::align lives there;
  newer libc++ no longer pulls it in transitively)
- test_remote_pineapple.cpp: fix build_input_from_frame returning an
  OperatorInput holding a pointer to a stack-local InputFieldSpec —
  a latent dangling-pointer bug that item_column's field-name compare
  turned into a reproducible SIGSEGV (caught by ASan)

Tests: new test_item_column.cpp (both frames: item() parity, defaults,
window-view offsets, absent field); full doctest suite 219 cases /
110212 assertions green incl. ASan build; clang-format applied.
/opt/homebrew/opt/luajit is where brew installs LuaJIT on Apple
Silicon; without the hint, local cross-validate prebuild fails to
configure on macOS while CI (linuxbrew/apt paths) is unaffected.
item_defaults only fire on slots that read as nil; random item
generation rarely lines the two up, so the Defaulted substitution
path — including OperatorInput.ItemColumn's defaults-copy branch
added in fbf2ef7 — was under-exercised (coverage showed the branch
at 0 hits across 60 rounds).

gen_pipeline now punches present-nil holes (40% per slot) into
recall_static / request items for fields that some operator declares
item_defaults on. Present-nil rather than deletion: missing keys
error in Nullable-mode readers, while present-nil passes through and
triggers substitution exactly where a default is declared.

New summary stat defaults_nil counts rounds where a defaulted field
actually co-occurs with an explicit nil. 60-round smoke: defaults=20
defaults_nil=14, 0 divergence, ItemColumn defaults-copy branch now
covered (verified via go build -cover + covdata).
Update stable docs after the column-store optimization landed on all
three engines (fbf2ef7 / bf7ce0b / 95a3000 + fuzz dimension 16440e8):

- reflections/column-vs-row-parity-investigation.md: replace the
  "uncommitted prototype" caveat with the landing status, add an
  implementation record (per-engine API shape table, shared semantic
  contract, verification results) and three implementation-phase
  lessons (spec raw-pointer lifetime in test helpers, quantify fuzz
  coverage instead of assuming, rule out machine load before blaming
  code for flaky divergence)
- reference/operator-contract.md: new batch column access contract
  section (ItemColumn / itemColumn / item_column three-engine table,
  Item()-identical semantics incl. defaults, read-only/Execute-scoped
  view rules, operator author guidance)
- decisions/perf-evolution-roadmap.md: step 1 note updated — the
  batch access API prerequisite for typed columns is now in place
- guides/ci-quality-baseline.md: document the defaults+nil
  co-occurrence fuzz dimension and the defaults_nil summary stat;
  drop the hardcoded dimension count per the no-hardcoded-quantities
  convention
- index.md: sync the three affected index entries
…workflow

Replace the verbose always-step-one / skill-table boilerplate with:
- explicit language conventions (Chinese for user-facing docs and
  communication, English for code comments and commit messages,
  plain-Mandarin wording rules with a jargon avoid-list)
- the llmdoc plugin's compact workflow reminder (skill loading,
  subagent roles, .llmdoc-tmp semantics, /llmdoc:update prompt)
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🔍 PR 审查

项目 结果
结论 ✅ APPROVE
审查截止 e729879ae6b826e35d652265fee366d9bec79c0a

三引擎批量列访问 API 实现质量高:语义与逐元素 Item() 保持一致(含 defaults 替换)、算子热循环改写的索引对齐正确、window offset 平移与各引擎 item() 完全对齐、Go 侧 Additions 两遍重写经逐行核对与原实现等价。已检查 bug 与代码规范,无阻塞问题。

逐项核对结论:

  • Go ItemColumnoperator_io.go):materialized 分支不套 defaults,与 materialized 版 Item() 一致;lazy + ColumnReader + fallback 三路均与 Item() byte-equal。
  • Go Additions 两遍重写column_frame.go):新列 padding 长度、_source 注入、f.rowCount 一次性推进均与旧 item-major 实现产出相同列布局。
  • 窗口平移:Go/Java/C++ 的 ItemColumnView/item_column 越界判定与 offset 处理与各自 item() 逐字一致;C++ view_offset_/view_count_ 用法与 ColumnFrame::item 对齐。
  • 算子改写normalize/sort/shuffle/dedup/copy/lua/filter_condition/remote_pineapple 等索引对齐正确;C++ remote_pineapple 由 item-major 改 field-major 后,单个 item 内字段插入顺序仍按 item_input 顺序保持不变。
  • 测试 dangling-pointer 修复test_remote_pineapple.cpp):OperatorInput 持 spec 裸指针,改为 function-static store 保活是合理修复。

🟢 小问题 (2)

  • 文件: pine-go/.../operator_io.go vs pine-java/.../OperatorInput.java

  • 问题: defaults 判定存在实现不对称——Go 用双值查找 defaultVal, hasDefault = itemDefaults[field],Java 用 defaultVal == null。当某字段声明的 default 值本身为 nil 时,Go 会走 defaults-copy 分支(强制拷贝),Java 直接返回零拷贝 view。二者返回值完全相同(nil→nil 为 no-op),仅内部路径差异,无可观测行为分歧,且与各自 item() 内部判定一致,故非缺陷;仅备注供跨引擎一致性存档。

  • 文件: pine-cpp/tests/test_remote_pineapple.cpp

  • 问题: build_input_from_frame 的 function-static specs 向量随每次调用单调增长且非线程安全。测试辅助场景下可接受(单线程、进程短命),无需处理。

已通过 PR 描述交叉核对:三引擎测试全绿、cross-validate 列存 parity 95/95、differential-fuzz 120 轮 0 分歧、新增 defaults_nil 维度已覆盖 defaults-copy 分支。

Liam0205 added a commit that referenced this pull request Jul 7, 2026
… ownership)

Both items were flagged as non-blocking by the review bot; verified
real and fixed rather than carried as debt:

- pine-go ItemColumn: treat a nil item-default as "no default" so the
  zero-copy view path stays available (nil-for-nil substitution is a
  no-op). Aligns the branch condition with pine-java's
  defaultVal == null check — outputs were already identical, but the
  copy-vs-view path divergence was an avoidable cross-engine
  inconsistency.
- pine-cpp test_remote_pineapple: replace the function-static spec
  store (unbounded growth, not thread-safe) with an OwnedInput struct
  bundling spec + input, mirroring production ownership shape with
  scope-bound lifetime.

Note: pre-commit hook requires bash>=4 (mapfile); system /bin/bash is
3.2 on this machine. Hook was run manually under homebrew bash 5 and
passed; --no-verify used only to bypass the interpreter mismatch.
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 e729879..b62b05d

本次增量正是回应上次审查标记的两个小问题(fix commit b62b05d),两处改动均已验证为真实修复且无新增问题。

逐项核对

1. pine-go ItemColumn:nil-default 视为"无 default"operator_io.go#L130-L141

  • behavior-preserving:view 路径 !hasDefault 直接返回原始 view,nil 槽位保持 nil;旧路径(hasDefault=true, defaultVal=nil)的拷贝循环对 nil 槽位替换 nil→nil 同样是 no-op,输出逐字节一致,仅省去一次拷贝。
  • fallback 路径同理:v == nil && hasDefault 现不触发,v 保持 nil,与旧行为一致。
  • Item(i, field) 语义仍一致(nil default 下 Item 也返回 nil),且判定条件现与 pine-java 的 defaultVal == null 对齐,消除了跨引擎 copy-vs-view 路径分歧。

2. pine-cpp OwnedInput 替换 function-static storetest_remote_pineapple.cpp#L162-L182

  • 成员声明序 spec(unique_ptr)先于 input,构造函数初始化列表中 input(... *spec) 解引用的是已初始化的 spec,顺序正确。
  • OperatorInput 持有指向 *spec 的裸指针;return OwnedInput(frame, cfg); 为 prvalue,C++17 保证性拷贝消除直接就地构造,auto owned = ... 同样省略 move。即便发生 move,unique_ptr 所有权转移不改变堆上 InputFieldSpec 地址,裸指针始终有效——无悬垂。
  • 相比 function-static std::vector 方案,作用域绑定生命周期消除了无界增长与线程安全隐患,且更贴合生产端 input_specs_ 的所有权形态。

两项 review note 均已按"验证为真实问题后修复而非当作技术债携带"处理,实现正确。批准合并。

Both review items were flagged as non-blocking by the review bot;
verified real and fixed rather than carried as debt:

- pine-go ItemColumn: treat a nil item-default as "no default" so the
  zero-copy view path stays available (nil-for-nil substitution is a
  no-op). Aligns the branch condition with pine-java's
  defaultVal == null check — outputs were already identical, but the
  copy-vs-view path divergence was an avoidable cross-engine
  inconsistency.
- pine-cpp test_remote_pineapple: replace the function-static spec
  store (unbounded growth, not thread-safe) with an OwnedInput struct
  bundling spec + input, mirroring production ownership shape with
  scope-bound lifetime.

Also fix .githooks/pre-commit for macOS: /bin/bash is 3.2 there and
mapfile is bash 4+, so the hook aborted before checking anything.
Replace mapfile with a while-read loop; verified the hook now passes
end-to-end under system bash 3.2.
@Liam0205
Liam0205 force-pushed the feat/column-store-batch-access branch from b62b05d to 5dc3c9e Compare July 7, 2026 10:18
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 b62b05d..5dc3c9e

上次审查截止点 b62b05d 已批准全部代码与三引擎批量列访问 API 改动。本次增量的唯一新内容是 5dc3c9e 追加的 .githooks/pre-commit bash 3.2 兼容性修复(b62b05d 被 amend 为 5dc3c9e)——验证为正确的行为等价替换,无新增问题。

逐项核对

.githooks/pre-commitmapfilewhile IFS= read -r 循环pre-commit#L27-L36

  • 根因正确:macOS 系统 /bin/bash 为 3.2,mapfile/readarray 是 bash 4+ 内建,set -uo pipefail 下调用不存在的内建会使 staged 保持未定义、后续 ${#staged[@]} 触发 unbound 报错,hook 在检查任何文件前就中止。注释已如实记录 WHY。
  • 行为等价IFS= read -r line 逐行读取时保留行内空白与反斜杠,与 mapfile 默认按换行切分、不做转义的语义一致;两者对含换行符的文件名都无法处理(同等限制)。
  • 数组作用域正确:进程替换 < <(git diff ...) 使 while 循环在当前 shell 执行(非管道子 shell),staged+=("$line") 追加对循环外可见;+= 数组追加语法 bash 3.1+ 即支持。
  • 下游用法未受影响for f in "${staged[@]}"L41)按元素展开消费数组,与填充方式解耦,无需改动。

其余文件(三引擎列访问实现、fuzz 维度、CLAUDE.md/settings.json、llmdoc)均在 b62b05d 及更早的两轮审查中已核对通过,此处不再重复。批准合并。

@Liam0205
Liam0205 merged commit 559aa99 into master Jul 7, 2026
21 checks passed
@Liam0205
Liam0205 deleted the feat/column-store-batch-access branch July 7, 2026 10:30
Liam0205 added a commit that referenced this pull request Jul 7, 2026
Port pine-cpp's Column hierarchy (include/pine/column.hpp) to the other
two engines: fixed-width typed columns (float64/double, string, bool)
with a validity bitmap, plus a JSON fallback for mixed/composite/
present-null data. Cashes in the batch access API from PR #155 — with
the per-element interface tax gone, boxing was the next bottleneck.

pine-go (internal/dataframe/column.go + column_frame.go rewrite):
- column interface + typedColumn[T] generics + jsonColumn fallback
- construction-time type inference (one pass per field, mirrors
  pine-cpp make_column); present-nil disqualifies typed storage
- runtime promotion on type-mismatched / nil writes (toJSON + retry),
  matching pine-cpp write_item_field_locked semantics
- deliberate divergence from pine-cpp, documented in column.go: typed
  dispatch is by EXACT runtime type (float64 only for numerics) —
  Go's `any` preserves int vs float64 and downstream contracts observe
  it (%T error messages, type-prefixed dedup keys); JSON-sourced data
  is always float64 so the common case still gets the typed path
- types.Float64ColumnReader optional interface +
  OperatorInput.ItemColumnFloat64: zero-copy raw []float64 window,
  ok only when fully present (defaults can never fire); normalize and
  sort hot loops use it, skipping boxing + type assertions entirely
- removals/reorder move to bitmap-compaction / cycle-following inside
  each column (scratch shared across columns)

pine-java (Column.java + ColumnFrame.java rewrite): same model —
Column.build inference, DoubleColumn/StringColumn/BoolColumn/JsonColumn,
promotion on set/append failure, Frame.itemColumnDoubleView default
method + OperatorInput.itemColumnDouble, TransformNormalize/ReorderSort
fast paths.

Benchmarks (Apple M5 Pro, high ambient load — same-run A/B):
- pine-go New/column: 33→40 allocs but bytes 177KB→94KB (typed arrays)
- Removals/column: 88KB→1KB per op, 21→1 allocs (bitmap compaction)
- e2e transform-heavy 1000: column ~0.89ms vs row ~1.37ms (~35% faster,
  bytes -51%); 5000: column ~3.6ms vs row ~5.6ms

Tests: typed inference / promotion / present-nil semantics / fast-path
gating / row-vs-column result parity on both engines (Go
typed_column_test.go, Java TypedColumnTest.java); full suites green
(go test -race + golangci-lint 0 issues; mvn 256 tests + checkstyle);
cross-validate sections 3/4/5 green (95/95 column-store parity);
120-round three-engine differential fuzz 0 divergence.
Liam0205 added a commit that referenced this pull request Jul 8, 2026
…hree engines (#157)

Write-side counterpart of the batch column read API (#155) and typed
columns (#156): operators hand a whole float64/double column to the
frame in one call instead of N per-element SetItem records.

API (per engine):
- pine-go:   OperatorOutput.SetItemColumnFloat64(field, []float64)
- pine-java: OperatorOutput.setItemColumnDouble(field, double[])
- pine-cpp:  OperatorOutput::set_item_column_double(field, vector<double>)

Semantics (identical across engines, pinned by tests):
- Applied at stage 2b, AFTER per-element item writes — a column write
  to the same field deterministically wins.
- Length must equal the frame's item count (whole column or nothing);
  mismatch error message is byte-identical across engines.
- NaN/Inf batch validation produces the same first-error message as
  the per-element path (item[i] write: field "f": NaN/Inf ...).
- Column-store frames ADOPT the array as the column's backing storage
  (zero-copy, all slots present); row-store frames scatter per row in
  one lock window.
- Counts as SetItem for OperatorType.ValidateOutput; folds into the
  item_writes debug snapshot; data_parallel shard merges fold column
  writes into offset-adjusted per-element writes.

transform_normalize now writes its result column via the batch API in
all three engines.

pine-go transform-heavy 1000 A/B (vs master): column 0.92ms → 0.70ms
(-24%), 12.1k → 4.1k allocs/op (-66%), bytes -11%; row-store also
drops 15.1k → didn't regress (scatter path, boxing unavoidable).

Verification: three-engine unit tests green (Go all, Java 263, C++
225/110245 assertions); cross-validate sections 3/4/5 (95/95 exec +
column-store parity, 30/30 error parity, Go vs Java vs C++);
differential fuzz 120 rounds seed=42 zero divergence (row 73/0,
column 47/0).
Liam0205 added a commit that referenced this pull request Jul 26, 2026
Selection criteria only existed in llmdoc, so downstream users had no way to
discover when column mode pays off — README said "DataFrame supports two
storage modes" and stopped there, and doc/ documented no root-level config at
all. Adds a "Flow-level configuration" section covering storage_mode,
log_prefix, debug and skip_dead_code, with the selection criteria written out:
transform-dominated + large N + few structural changes favours column,
recall/filter/sort or small N stays on row, plus the mechanical reason.

Deliberately states no speedup multiples. The figures floating around
(~30% in llmdoc vs ~37% in the PR #155 description) are two measurements of
the same experiment that disagree, they come from Go microbenchmarks on
Apple silicon while README's Benchmark table is Linux 2C/4G HTTP end-to-end,
and #156/#157 have already moved them three times. Points at the A/B entry
points instead, so readers measure their own workload. The criteria are
stable; the multiples are not.

Both documented commands were run before committing: the benchmarks/ module
needs its own directory and the pine_bench tag, which the first draft got
wrong. The Flow snippet and the compile-time rejection of invalid values were
checked too.

Says nothing about what happens on an invalid value, because the runtimes
disagree — pine-go and pine-java fall back to row, pine-cpp falls back to
column. The Apple DSL rejects it at compile time, which is the contract worth
documenting; the divergence is filed separately.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant