Skip to content

Commit 6d75a6e

Browse files
committed
Restore edition-aware syntax compatibility with 0.25
Keep legacy public generic declarations valid in edition 2026 and bind the 2027 layout rule to the template owner across compiler and interface consumers. Preserve generic constraint spelling and explicit abilities during formatting. Accept ScriptHash in native lifecycle lock clauses with fixed-byte witnesses. Add compiler, LSP, WASM, Registry, checker, and real-VM regressions. Record the English syntax audit and refresh the playground bundle and documentation pin.
1 parent 9007851 commit 6d75a6e

27 files changed

Lines changed: 405 additions & 82 deletions

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ for the accepted compatibility boundary and publication constraints.
99
The entries below retain the evidence state at each implementation step. The
1010
release notes and capability ledger record the current acceptance state.
1111

12+
- Restore the edition 2026 generic declaration contract from 0.25; apply the
13+
stricter public layout declaration boundary only to edition 2027 owners.
14+
Preserve constraint spelling and explicit abilities during formatting, and
15+
accept nominal `ScriptHash` inputs in native `exact_hash` lifecycle clauses.
16+
See [the syntax compatibility audit](docs/CELLSCRIPT_0_25_0_30_SYNTAX_AUDIT.md).
17+
1218
- Make cost evidence mandatory in CI/backend and both release gates: missing
1319
RISC-V target libraries or the selected Rust toolchain's `llvm-strip` fail
1420
instead of silently skipping. Require a fresh execution report, independent

‎crates/cellscript-artifact-checker/src/checker.rs‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,6 +1542,11 @@ fn validate_public_interface_metadata(metadata: &Value, module: &str, expected_h
15421542
{
15431543
return Err(metadata_binding_error("compile metadata public interface has an invalid schema, version, or module identity"));
15441544
}
1545+
let edition = interface.get("edition").and_then(Value::as_str);
1546+
if !matches!(edition, Some("2026" | "2027")) || edition != metadata.get("edition").and_then(Value::as_str) {
1547+
return Err(metadata_binding_error("public interface edition must match compile metadata"));
1548+
}
1549+
let strict_layout = edition == Some("2027");
15451550
let canonical = canonical_json_value(&Value::Object(interface.clone()));
15461551
let canonical_bytes = serde_json::to_vec(&canonical)
15471552
.map_err(|error| metadata_binding_error(format!("failed to serialize canonical public interface: {error}")))?;
@@ -1555,7 +1560,7 @@ fn validate_public_interface_metadata(metadata: &Value, module: &str, expected_h
15551560
let callables = validate_public_interface_items(interface.get("callables"), "callable")?;
15561561
for item in types {
15571562
let identity = item.get("identity").and_then(Value::as_str).unwrap_or("<unknown>");
1558-
validate_public_type_parameters(item.get("type_parameters"), &format!("{identity}.type_parameters"), true)?;
1563+
validate_public_type_parameters(item.get("type_parameters"), &format!("{identity}.type_parameters"), strict_layout)?;
15591564
validate_public_value_abilities(item.get("value_abilities"), &format!("{identity}.value_abilities"))?;
15601565
}
15611566
for item in callables {
@@ -8049,6 +8054,7 @@ mod tests {
80498054
"schema": "cellscript-package-interface-v3",
80508055
"version": 3,
80518056
"module": "api",
8057+
"edition": "2027",
80528058
"types": [{
80538059
"identity": "api::Pair",
80548060
"type_parameters": [{
@@ -8062,7 +8068,7 @@ mod tests {
80628068
"callables": []
80638069
});
80648070
let hash = public_interface_hash(&interface);
8065-
let metadata = serde_json::json!({ "public_interface": interface });
8071+
let metadata = serde_json::json!({ "edition": "2027", "public_interface": interface });
80668072
validate_public_interface_metadata(&metadata, "api", &hash).unwrap();
80678073

80688074
let mut compact_machine_form = metadata.clone();
@@ -8077,6 +8083,11 @@ mod tests {
80778083
let unsafe_hash = public_interface_hash(&unsafe_layout["public_interface"]);
80788084
let error = validate_public_interface_metadata(&unsafe_layout, "api", &unsafe_hash).unwrap_err();
80798085
assert!(error.message.contains("public layout boundary"), "{}", error.message);
8086+
unsafe_layout["public_interface"]["edition"] = serde_json::json!("2026");
8087+
let legacy_hash = public_interface_hash(&unsafe_layout["public_interface"]);
8088+
assert!(validate_public_interface_metadata(&unsafe_layout, "api", &legacy_hash).unwrap_err().message.contains("edition"));
8089+
unsafe_layout["edition"] = serde_json::json!("2026");
8090+
validate_public_interface_metadata(&unsafe_layout, "api", &legacy_hash).unwrap();
80808091
}
80818092

80828093
#[test]

‎crates/cellscript-wasm/src/lib.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,15 @@ mod tests {
386386
assert_eq!(result["runtime"]["fail_closed_obligations"], serde_json::json!([]));
387387
}
388388

389+
#[test]
390+
fn wasm_preserves_legacy_public_generics_in_2026_only() {
391+
let source = "module legacy\npublic struct Box<T: copy> has copy { value: T }\naction verify() -> u64 { verification let b: Box<u64> = Box<u64> { value: 7 } return b.value }";
392+
let legacy: serde_json::Value = serde_json::from_str(&compile_metadata_json(source, "2026", None)).unwrap();
393+
assert!(legacy.get("error").is_none(), "{legacy}");
394+
let strict: serde_json::Value = serde_json::from_str(&compile_metadata_json(source, "2027", None)).unwrap();
395+
assert!(strict.get("error").is_some(), "{strict}");
396+
}
397+
389398
#[test]
390399
fn wasm_playground_accepts_the_canonical_public_value_generic_surface() {
391400
let source = r#"
@@ -514,6 +523,9 @@ type_script TokenTransfer on type_group<Token> {
514523
let result: serde_json::Value = serde_json::from_str(&compile_metadata_json(source, "2027", None)).unwrap();
515524
assert_eq!(result["edition"], "2027");
516525
assert_eq!(result["actions"][0]["name"], "transfer");
526+
let typed_source = source.replace("recipient: Address", "recipient: ScriptHash");
527+
let typed: serde_json::Value = serde_json::from_str(&compile_metadata_json(&typed_source, "2027", None)).unwrap();
528+
assert_eq!(typed["actions"][0]["name"], "transfer", "{typed}");
517529

518530
#[cfg(feature = "language-service")]
519531
{
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# 0.25 to 0.30 syntax compatibility audit
2+
3+
The 2026 edition retains the checked source meaning of 0.25. New 2027 authoring
4+
forms remain opt-in. Compiler requirements, package locks, source editions,
5+
and artifact compatibility are separate axes; accepting old source does not
6+
promise identical ELF bytes across compiler releases.
7+
8+
## Comparison boundary
9+
10+
The audit compared tag `v0.25.0` (`3b481eb019a1fdc358d4efbe8340d28d3e6a9441`)
11+
with the pre-fix 0.30 candidate `9007851852072f6757686bdea2abfb79d87269c2`.
12+
The original 0.25 compiler was built from that exact source archive. Of 86
13+
historical source samples compiled outside their original package contexts,
14+
52 compiled with 0.25 and all 52 also compiled with the candidate. This is a
15+
bounded acceptance comparison, not a proof of semantic equivalence for all
16+
programs. Package-context failures from old lock schemas were classified
17+
separately; intentional repinning uses `cellc lock` or `cellc update`. The same
18+
86-source comparison was repeated after the fixes: all 52 old-accepted sources
19+
still compile. The exact original 0.25 compiler also successfully generated
20+
ELF from the historical generic seed after formatting with the fixed compiler.
21+
22+
Targeted examples exposed three gaps that the broad historical sample did not
23+
cover. The regression tests now enforce the following boundaries.
24+
25+
## Public generic declarations
26+
27+
This 0.25 program remains valid in edition 2026:
28+
29+
```cellscript
30+
module legacy
31+
struct Box<T: copy> has copy { value: T }
32+
action main() -> u64 {
33+
verification
34+
let value: Box<u64> = Box<u64> { value: 7 }
35+
return value.value
36+
}
37+
```
38+
39+
Unconstrained public templates remain accepted in 2026 as well. Concrete
40+
specializations still enforce declared abilities, bounded monomorphization,
41+
layout validity, phantom restrictions, and the prohibition on hiding Cells.
42+
Edition 2027 requires public non-phantom layout parameters to declare `fixed`,
43+
`serializable`, and `non_linear`. This stricter rule belongs to the template
44+
owner's edition, including imports across editions.
45+
46+
The compiler, canonical interface validator, Registry API, and independent
47+
artifact checker agree on this distinction. Current interface editions must
48+
match their enclosing metadata or publication record. Registry publication
49+
continues to accept only its existing supported edition; this fix does not
50+
expand Registry admission to preview packages.
51+
52+
## Full Script hashes in native lifecycle syntax
53+
54+
Both ordinary replacement relations and native `type_script` lifecycle
55+
clauses accept `ScriptHash` for `exact_hash(...)`. Native replacement, pool,
56+
and fresh-output paths share the fixed 32-byte hash representation. Existing
57+
native-preview `Address` and `Hash` arguments remain compatibility inputs.
58+
Ordinary replacement relations retain their stricter nominal-domain check:
59+
use `ckb::script_hash(hash)` when explicitly interpreting a trusted raw `Hash`
60+
as a complete Script hash. No arbitrary integer or unrelated nominal value
61+
is accepted as a lock hash.
62+
63+
The native witness builder uses `EntryWitnessArg::Bytes` for the nominal
64+
`ScriptHash` value, with exactly 32 bytes. This matches the existing nominal
65+
fixed-byte encoding rather than introducing a new wire ABI. Real CKB-VM tests
66+
exercise typed fresh outputs and replacement group binding.
67+
68+
## Formatting is not a source migration
69+
70+
`cellc fmt` preserves expanded generic constraints and explicit `has` clauses.
71+
If the source already uses `fixed_value`, it keeps that compact spelling.
72+
Formatting does not introduce `fixed_value` into a 0.25-compatible file or
73+
remove an explicit ability declaration that an older compiler needs. Compact
74+
and expanded constraints still produce the same canonical interface identity.
75+
This applies to the shared formatter used by CLI and editor consumers.
76+
77+
Opting into new shorthand or inferred abilities is an intentional source
78+
change. It requires a compatible compiler requirement; formatting alone does
79+
not change that requirement or repin a package.
80+
81+
## Regression evidence
82+
83+
- `tests/syntax_compatibility.rs`: legacy declarations, strict 2027 rejection,
84+
formatter stability and interface identity, native hash-domain acceptance.
85+
- `compile_file_monomorphizes_imported_generic_types_and_functions_in_their_owner_module`:
86+
dependency-owned contracts across both importer/owner edition directions.
87+
- `tests/native_group_binding.rs` and `tests/native_lifecycle_binding.rs`:
88+
actual CKB-VM typed lock-hash execution and adversarial transaction shapes.
89+
- Interface, independent checker, Registry, and WASM tests cover the consumer
90+
boundaries; the syntax matrix includes a nominal ScriptHash native seed.
91+
92+
Local gate results are recorded with the implementing change. Branch pushes
93+
are separate from publication: this work creates no release tag, invokes no
94+
Release workflow, and does not publish packages or trigger TAC.

‎docs/CELLSCRIPT_0_30_CAPABILITY_LEDGER.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,16 @@ The machine ledger is the detailed source of truth. At this candidate point:
9292
likewise reports no moderate/high/critical advisories after its release-tool
9393
refresh; its three remaining low findings are the same unsafe-fix class.
9494

95-
The current product gitlinks supersede the historical recovery pins above:
95+
The pre-syntax-audit candidate superseded the historical recovery pins above:
9696
VS Code `ee0f259a` and website `6a61e49` were pushed and read back with root
9797
`74cff5fd` on 2026-09-19. That candidate passed `dev`, full `ci` within
9898
`release`, `backend`, and `release` on 2026-09-18. Its canonical WASM is
9999
571,696 bytes gzip with SHA-256
100100
`419e8260c3c6acc9f950adf422de90163533c14af96043ebb7429f935b1393d6`.
101+
The subsequent [syntax compatibility fixes](CELLSCRIPT_0_25_0_30_SYNTAX_AUDIT.md)
102+
rebuild the current workspace WASM to 571,644 bytes gzip, SHA-256
103+
`8df77dde17da89b14dc31e1f08cb14e262a7e35516f415783b20f2bd68fcdb0d`.
104+
The historical release-gate result above belongs to its stated candidate.
101105
The [dated issue audit](releases/CELLSCRIPT_0_30_ISSUE_STATUS.md) records all
102106
24 issue dispositions and the remaining publication boundary.
103107

‎docs/CELLSCRIPT_2027_PREVIEW_GRAMMAR.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ resource Token has store, replace, relock {
6363
type_script TokenTransfer on type_group<Token> {
6464
entry transfer(
6565
input token: Token from group_input[0],
66-
witness recipient: Address from group_witness.input_type,
66+
witness recipient: ScriptHash from group_witness.input_type,
6767
output next: Token from group_output[0],
6868
) {
6969
verify {
@@ -282,7 +282,8 @@ rules:
282282
output-producing dispositions follow declared `group_output` order so the
283283
checked backend index and source role cannot diverge;
284284
every schema field and fixed envelope clause appears exactly once in
285-
canonical order; every `exact_hash(...)` subject has type `Address`; and
285+
canonical order; `exact_hash(...)` accepts the nominal `ScriptHash` domain;
286+
the native preview also retains `Address` and `Hash` inputs for compatibility; and
286287
`effects` is final and non-empty;
287288
- a `pool` has non-empty, unique, explicitly named input and output sets; at
288289
least one unsigned numeric schema field uses `= conserve`, which generates
@@ -471,7 +472,7 @@ The following remain outside `preview4`:
471472
- foreign/open roles, Script handles, ProtocolBundle, or `.celltx`
472473
choreography;
473474
- non-positional selectors and bounded variable-cardinality native roles;
474-
- Script-valued lock construction beyond the current Address-based transfer
475+
- Script-valued lock construction beyond the current full-hash-based transfer
475476
primitive;
476477
- audit evidence kinds beyond the metadata-only `external_policy(subject)`
477478
declaration;

‎docs/CELLSCRIPT_PUBLIC_VALUE_GENERICS.md‎

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ expanded abilities is a duplicate-constraint error. Combining `cell` and
5050

5151
The expanded spelling remains accepted. Compact and expanded source forms
5252
produce the same AST constraints, public interface, interface hash, typed
53-
instantiation evidence, IR, and artifact. The formatter writes the compact
54-
profile.
53+
instantiation evidence, IR, and artifact. The formatter preserves whether the
54+
source used the compact profile or expanded constraints, and retains explicit
55+
`has` clauses so formatting does not raise the minimum compiler version.
5556

5657
## Visibility and layout boundary
5758

@@ -62,10 +63,12 @@ profile.
6263
- Edition 2026 retains its historical public-by-default behavior, although new
6364
package APIs should spell visibility explicitly.
6465

65-
Every non-phantom parameter of a public generic struct or enum must declare at
66-
least `fixed + serializable + non_linear`. `fixed_value` is the normal spelling
66+
In edition 2027, every non-phantom parameter of a public generic struct or enum
67+
must declare at least `fixed + serializable + non_linear`. Use `fixed_value`
6768
when the parameter also supports copy, drop, and store. Missing public layout
68-
requirements fail with `E2110` and point to `fixed_value`.
69+
requirements fail with `E2110` and point to `fixed_value`. Edition 2026 retains
70+
the 0.25 declaration contract, including unconstrained public templates;
71+
concrete specializations still undergo ability, layout, and hidden-Cell checks.
6972

7073
A phantom parameter contributes to canonical type identity and occupies no
7174
serialized bytes. Using a phantom parameter in a field remains `E2110`.
@@ -135,16 +138,15 @@ monomorphization identities in typed semantics.
135138

136139
## Migration
137140

138-
The existing expanded nightly spelling remains source-compatible. Run
139-
`cellc fmt` to convert an exact six-constraint list to `fixed_value` and remove
140-
an explicit generic `has` clause when it exactly matches structural derivation.
141-
The formatter round-trip test proves this rewrite is deterministic and does not
142-
change the canonical interface identity.
141+
The expanded 0.25 spelling remains source-compatible under edition 2026.
142+
`cellc fmt` preserves expanded constraints and explicit `has` clauses. Adopting
143+
`fixed_value` or removing an explicit ability clause is an intentional source
144+
migration requiring a compiler that supports those forms.
143145

144-
Packages with public generic layouts that omit `fixed`, `serializable`, or
145-
`non_linear` must add the missing contract or make the template
146-
`public(package)`/`private`. This is an intentional stabilization error rather
147-
than an implicit change to the accepted type set.
146+
When opting into edition 2027, public generic layouts must add any missing
147+
`fixed`, `serializable`, or `non_linear` constraints, or change visibility to
148+
`public(package)`/`private`. Dependency templates use their owning package
149+
edition; the importer cannot silently select a stricter declaration contract.
148150

149151
## Evidence boundary
150152

‎docs/wiki/Tutorial-16-Generics-Interfaces-and-Typed-Artifacts.md‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,18 @@ private fn internal_identity(value: u64) -> u64 {
2727
`fixed_value` is the source shorthand for the canonical expanded constraint set
2828
`copy + drop + store + fixed + serializable + non_linear`. Machine-readable
2929
interfaces always contain the expanded list. When a generic struct or enum
30-
omits `has`, the compiler derives its abilities from the field contract; the
31-
formatter therefore removes an equivalent redundant `has` clause.
30+
omits `has`, the compiler derives its abilities from the field contract. The
31+
formatter preserves explicit `has` clauses and the chosen constraint spelling
32+
to avoid silently raising the source minimum compiler version.
3233

3334
CellScript monomorphizes concrete value uses before IR lowering. The compiler
3435
records every instantiation and applies fixed nesting, count, and identity-size
3536
budgets. Ordinary generic containers cannot hide a Cell-backed value.
36-
Public generic layouts require every non-phantom type parameter to be fixed,
37-
serializable, and non-linear. Public templates may be imported from
38-
dependencies; specializations remain in the owning module and do not become
39-
public-interface entries in the consumer.
37+
Edition 2027 public generic layouts require every non-phantom type parameter
38+
to be fixed, serializable, and non-linear. Edition 2026 preserves the 0.25
39+
declaration rules while validating every concrete specialization. Public
40+
templates may be imported from dependencies. Their specializations remain in
41+
the owning module and do not become public-interface entries in the consumer.
4042

4143
Value abilities are not Cell authority. `copy`, `drop`, `fixed`,
4244
`serializable`, and `non_linear` describe ordinary values; `create`, `consume`,

‎services/registry-api/src/domain.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -935,8 +935,12 @@ function validateTypeParameters(value: unknown, label: string, layoutType: boole
935935
}
936936

937937
function validatePublicInterfaceGenerics(publicInterface: Record<string, unknown>): void {
938+
const edition = publicInterface["edition"];
939+
if (edition !== "2026" && edition !== "2027") {
940+
throw new ApiError(400, "invalid_public_interface", "unsupported public interface edition");
941+
}
938942
for (const item of interfaceItems(publicInterface["types"], "types").values()) {
939-
validateTypeParameters(item["type_parameters"], `${requireString(item, "identity")}.type_parameters`, true);
943+
validateTypeParameters(item["type_parameters"], `${requireString(item, "identity")}.type_parameters`, edition === "2027");
940944
canonicalValueAbilities(item["value_abilities"], `${requireString(item, "identity")}.value_abilities`);
941945
}
942946
for (const item of interfaceItems(publicInterface["callables"], "callables").values()) {
@@ -1073,6 +1077,9 @@ function validateRegistryEntry(
10731077
validateHash(interfaceHash, "interface_hash", "invalid_interface_hash");
10741078
const publicInterface = assertPlainObject(published["interface"], "invalid_public_interface");
10751079
validatePublicInterfaceVersion(publicInterface);
1080+
if (publicInterface["version"] === 3 && publicInterface["edition"] !== published["edition"]) {
1081+
throw new ApiError(400, "invalid_public_interface", "public interface edition must match the published edition");
1082+
}
10761083
const computedInterfaceHash = ckbBlake2bHex(canonicalJson(publicInterface));
10771084
if (computedInterfaceHash.replace(/^0x/, "") !== interfaceHash.replace(/^0x/, "")) {
10781085
throw new ApiError(400, "interface_hash_mismatch", "interface_hash must bind the canonical public interface");

0 commit comments

Comments
 (0)