@ball-lang/compiler 1.7.5 → 1.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"preamble.d.ts","sourceRoot":"","sources":["../src/preamble.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,QA29C/B,CAAC"}
1
+ {"version":3,"file":"preamble.d.ts","sourceRoot":"","sources":["../src/preamble.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,QA++C/B,CAAC"}
package/dist/preamble.js CHANGED
@@ -1114,6 +1114,26 @@ function __ball_map_entries(m: any): any {
1114
1114
  return Object.entries(m).map(([k, v]) => ({ key: k, value: v }));
1115
1115
  }
1116
1116
 
1117
+ // Shared guard for the REMAINING map_* base-function-call cases
1118
+ // (map_get/map_set/map_delete/map_merge/map_length/map_is_empty/
1119
+ // map_contains_key/map_contains_value/map_foreach) that used to route a
1120
+ // bare map[key], Object.keys/values(map), or key in map straight to the
1121
+ // receiver with no type check at all -- silently returning undefined,
1122
+ // no-opping, or checking array-index membership instead of throwing on a
1123
+ // non-Map (issue #55's silent-degradation class, same family as #218's
1124
+ // map_keys/map_values/map_entries). Returns the validated Map/plain-object
1125
+ // itself (not a boolean) so a call site can keep using it directly, e.g.
1126
+ // __ball_require_map(x, 'map_get')[key].
1127
+ function __ball_require_map(v: any, opName: string): any {
1128
+ if (v instanceof Map) return v;
1129
+ if (typeof v !== 'object' || v === null || Array.isArray(v) ||
1130
+ v instanceof BallDouble || v instanceof Set ||
1131
+ v instanceof Number || v instanceof String || v instanceof Boolean) {
1132
+ throw new Error('type \'' + __ball_to_string(v) + '\' is not a Map (' + opName + ')');
1133
+ }
1134
+ return v;
1135
+ }
1136
+
1117
1137
  // ── Protobuf Struct/Value compatibility ─────────────────────────
1118
1138
  //
1119
1139
  // Dart's protobuf runtime wraps google.protobuf.Struct as a class
@@ -1 +1 @@
1
- {"version":3,"file":"preamble.js","sourceRoot":"","sources":["../src/preamble.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA29C5C,CAAC"}
1
+ {"version":3,"file":"preamble.js","sourceRoot":"","sources":["../src/preamble.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA++C5C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ball-lang/compiler",
3
- "version": "1.7.5",
3
+ "version": "1.7.6",
4
4
  "description": "Ball → TypeScript compiler. Consumes a Ball protobuf Program and emits idiomatic TypeScript via ts-morph. The canonical TS compiler for Ball — lives in TS land so TS syntax knowledge doesn't leak into other languages.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/compiler.ts CHANGED
@@ -4570,7 +4570,14 @@ function __isUnknownFnError(e: any): boolean {
4570
4570
  case "null_aware_call": {
4571
4571
  const target = f.get("target");
4572
4572
  const method = f.get("method");
4573
- if (!target || !method) return "/* null_aware_call missing */";
4573
+ // A bare `/* ... */` comment used to stand in for the missing
4574
+ // field — context-dependent behavior (silently becomes `undefined`
4575
+ // in return position, a hard-to-diagnose SyntaxError mid-expression)
4576
+ // instead of a clear compile-time error naming the malformed call
4577
+ // (#257).
4578
+ if (!target || !method) {
4579
+ throw new Error("TS compiler: null_aware_call requires a \"target\" and a \"method\" field");
4580
+ }
4574
4581
  const methodName = method.literal?.stringValue ?? "";
4575
4582
  const inputFields = call.input?.messageCreation?.fields ?? [];
4576
4583
  const otherArgs = inputFields
@@ -4582,13 +4589,17 @@ function __isUnknownFnError(e: any): boolean {
4582
4589
  case "null_aware_index": {
4583
4590
  const self_ = f.get("self") ?? f.get("target");
4584
4591
  const idx = f.get("index") ?? f.get("key");
4585
- if (!self_ || !idx) return "/* null_aware_index missing */";
4592
+ if (!self_ || !idx) {
4593
+ throw new Error("TS compiler: null_aware_index requires a \"self\"/\"target\" and an \"index\"/\"key\" field");
4594
+ }
4586
4595
  return `${this.expr(self_)}[${this.expr(idx)}]`;
4587
4596
  }
4588
4597
  case "null_aware_access": {
4589
4598
  const target = f.get("target");
4590
4599
  const fieldE = f.get("field");
4591
- if (!target || !fieldE) return "/* null_aware_access missing */";
4600
+ if (!target || !fieldE) {
4601
+ throw new Error("TS compiler: null_aware_access requires a \"target\" and a \"field\" field");
4602
+ }
4592
4603
  return `${this.expr(target)}?.${fieldE.literal?.stringValue ?? ""}`;
4593
4604
  }
4594
4605
  case "typed_list": {
@@ -4607,13 +4618,19 @@ function __isUnknownFnError(e: any): boolean {
4607
4618
  if (entryExprs.length === 0) return "new Map()";
4608
4619
  const pairs: string[] = [];
4609
4620
  for (const e of entryExprs) {
4610
- if (e.messageCreation) {
4611
- const mc = e.messageCreation;
4612
- const mFields = mc.fields ?? [];
4613
- const k = mFields.find((fd) => fd.name === "key")?.value;
4614
- const v = mFields.find((fd) => fd.name === "value")?.value;
4615
- if (k && v) pairs.push(`[${this.expr(k)}, ${this.expr(v)}]`);
4621
+ // A malformed entry (not a messageCreation, or missing key/value)
4622
+ // used to be silently dropped from the resulting Map instead of
4623
+ // surfaced fail loud on the malformed IR instead (#257).
4624
+ if (!e.messageCreation) {
4625
+ throw new Error("TS compiler: typed_map entry is not a key/value messageCreation");
4616
4626
  }
4627
+ const mFields = e.messageCreation.fields ?? [];
4628
+ const k = mFields.find((fd) => fd.name === "key")?.value;
4629
+ const v = mFields.find((fd) => fd.name === "value")?.value;
4630
+ if (!k || !v) {
4631
+ throw new Error("TS compiler: typed_map entry is missing \"key\" or \"value\"");
4632
+ }
4633
+ pairs.push(`[${this.expr(k)}, ${this.expr(v)}]`);
4617
4634
  }
4618
4635
  return `new Map([${pairs.join(", ")}])`;
4619
4636
  }
@@ -4943,20 +4960,26 @@ function __isUnknownFnError(e: any): boolean {
4943
4960
  case "map_get": {
4944
4961
  const map = f.get("map");
4945
4962
  const key = f.get("key");
4946
- if (map && key) return `${this.expr(map)}[${this.expr(key)}]`;
4963
+ // __ball_require_map fails loud on a non-Map instead of silently
4964
+ // returning undefined via bare bracket access (#257, same family
4965
+ // as #218's map_keys/map_values/map_entries).
4966
+ if (map && key) return `__ball_require_map(${this.expr(map)}, 'map_get')[${this.expr(key)}]`;
4947
4967
  return "undefined";
4948
4968
  }
4949
4969
  case "map_set": {
4950
4970
  const map = f.get("map");
4951
4971
  const key = f.get("key");
4952
4972
  const value = f.get("value");
4953
- if (map && key && value) return `(${this.expr(map)}[${this.expr(key)}] = ${this.expr(value)})`;
4973
+ if (map && key && value) return `(__ball_require_map(${this.expr(map)}, 'map_set')[${this.expr(key)}] = ${this.expr(value)})`;
4954
4974
  return "undefined";
4955
4975
  }
4956
4976
  case "map_contains_key": {
4957
4977
  const map = f.get("map");
4958
4978
  const key = f.get("key");
4959
- if (map && key) return `(${this.expr(key)} in ${this.expr(map)})`;
4979
+ // `in` throws for a primitive receiver already, but not for a List
4980
+ // (checks index membership instead of throwing "not a map") — the
4981
+ // require-map guard closes that gap (#257).
4982
+ if (map && key) return `(${this.expr(key)} in __ball_require_map(${this.expr(map)}, 'map_contains_key'))`;
4960
4983
  return "false";
4961
4984
  }
4962
4985
  case "map_keys": {
@@ -4981,22 +5004,24 @@ function __isUnknownFnError(e: any): boolean {
4981
5004
  }
4982
5005
  case "map_length": {
4983
5006
  const map = f.get("map") ?? f.get("value");
4984
- return map ? `Object.keys(${this.expr(map)}).length` : "0";
5007
+ return map ? `Object.keys(__ball_require_map(${this.expr(map)}, 'map_length')).length` : "0";
4985
5008
  }
4986
5009
  case "map_is_empty": {
4987
5010
  const map = f.get("map") ?? f.get("value");
4988
- return map ? `(Object.keys(${this.expr(map)}).length === 0)` : "true";
5011
+ return map ? `(Object.keys(__ball_require_map(${this.expr(map)}, 'map_is_empty')).length === 0)` : "true";
4989
5012
  }
4990
5013
  case "map_delete": case "map_remove": {
4991
5014
  const map = f.get("map");
4992
5015
  const key = f.get("key");
4993
- if (map && key) return `(() => { const __m = ${this.expr(map)}; const __k = ${this.expr(key)}; const __v = __m[__k]; delete __m[__k]; return __v; })()`;
5016
+ if (map && key) return `(() => { const __m = __ball_require_map(${this.expr(map)}, 'map_delete'); const __k = ${this.expr(key)}; const __v = __m[__k]; delete __m[__k]; return __v; })()`;
4994
5017
  return "undefined";
4995
5018
  }
4996
5019
  case "map_merge": {
4997
5020
  const l = f.get("left") ?? f.get("map");
4998
5021
  const r = f.get("right") ?? f.get("other");
4999
- if (l && r) return `{...${this.expr(l)}, ...${this.expr(r)}}`;
5022
+ // Spreading a non-object silently produces {} (or a partial merge)
5023
+ // instead of throwing — same class as the other map_* guards (#257).
5024
+ if (l && r) return `{...__ball_require_map(${this.expr(l)}, 'map_merge'), ...__ball_require_map(${this.expr(r)}, 'map_merge')}`;
5000
5025
  return "{}";
5001
5026
  }
5002
5027
  case "map_from_entries": {
@@ -5011,13 +5036,20 @@ function __isUnknownFnError(e: any): boolean {
5011
5036
  if (list) return `${this.expr(list)}.join('')`;
5012
5037
  return "''";
5013
5038
  }
5039
+ // Neither encoder ever emits these as a direct `std.<fn>` base-
5040
+ // function call — a Set method call (mySet.union(other), etc.)
5041
+ // routes through compileCall's generic "self"-field method dispatch
5042
+ // onto native JS Set.prototype methods instead, so this case is
5043
+ // never actually reached. The old fallback compiled it to a call on
5044
+ // a nonexistent bare identifier (a confusing runtime ReferenceError
5045
+ // if it WERE ever hit); fail loud at compile time instead, matching
5046
+ // the policy above (#257).
5014
5047
  case "set_add": case "set_remove": case "set_contains":
5015
5048
  case "set_union": case "set_intersection": case "set_difference":
5016
- case "set_length": case "set_is_empty": case "set_to_list": {
5017
- const allFields = call.input?.messageCreation?.fields ?? [];
5018
- const args = allFields.map((fd) => this.expr(fd.value)).join(", ");
5019
- return `/* std.${fn} */ ${sanitize(fn)}(${args})`;
5020
- }
5049
+ case "set_length": case "set_is_empty": case "set_to_list":
5050
+ throw new Error(
5051
+ `TS compiler: std.${fn} is not implemented (compileStdCall) Set method calls should route through native JS Set methods, not this base function`,
5052
+ );
5021
5053
  // I/O
5022
5054
  case "print_error": {
5023
5055
  const msg = f.get("message") ?? f.get("value");
@@ -5126,7 +5158,10 @@ function __isUnknownFnError(e: any): boolean {
5126
5158
  // match first and any duplicate entry here would be genuinely
5127
5159
  // unreachable dead code. Removed (#62 Phase-2c); only cases with
5128
5160
  // no outer-switch equivalent belong in this fallback.
5129
- case "map_contains_value": return `Object.values(${this.expr(f.get("map")!)}).includes(${this.expr(f.get("value")!)})`;
5161
+ // Same silent-degradation class as map_keys/map_values/map_entries
5162
+ // (#218) — the sibling case that fix missed, since it lives in
5163
+ // this SECOND nested switch under a different function name (#257).
5164
+ case "map_contains_value": return `Object.values(__ball_require_map(${this.expr(f.get("map")!)}, 'map_contains_value')).includes(${this.expr(f.get("value")!)})`;
5130
5165
  case "list_insert": return `(${this.expr(f.get("list")!)}.splice(${this.expr(f.get("index")!)}, 0, ${this.expr(f.get("value")!)}), ${this.expr(f.get("list")!)})`;
5131
5166
  case "list_remove_at": return `${this.expr(f.get("list")!)}.splice(${this.expr(f.get("index")!)}, 1)[0]`;
5132
5167
  case "list_clear": return `(${this.expr(f.get("list")!)}.length = 0, ${this.expr(f.get("list")!)})`;
@@ -5154,7 +5189,7 @@ function __isUnknownFnError(e: any): boolean {
5154
5189
  case "map_foreach": {
5155
5190
  const map = this.expr(f.get("map") ?? f.get("value")!);
5156
5191
  const cb = this.expr(f.get("function") ?? f.get("callback") ?? f.get("value")!);
5157
- return `Object.entries(${map}).forEach(([k, v]) => ${cb}(k, v))`;
5192
+ return `Object.entries(__ball_require_map(${map}, 'map_foreach')).forEach(([k, v]) => ${cb}(k, v))`;
5158
5193
  }
5159
5194
  case "list_reversed": return `[...${this.expr(f.get("list")!)}].reverse()`;
5160
5195
  case "compare_to": {
@@ -5225,10 +5260,18 @@ function __isUnknownFnError(e: any): boolean {
5225
5260
  );
5226
5261
  return `(() => { const __r: any[] = []; ${body} return __r; })()`;
5227
5262
  }
5228
- default: {
5229
- const args = Array.from(f.values()).map((e) => this.expr(e)).join(", ");
5230
- return `/* std.${fn} */ ${sanitize(fn)}(${args})`;
5231
- }
5263
+ default:
5264
+ // Fail loud (repo CLAUDE.md): never emit a bare/undefined
5265
+ // identifier for an unimplemented std function — mirrors
5266
+ // compileMemoryCall's rule above. The old
5267
+ // `/* std.${fn} */ ${sanitize(fn)}(${args})` fallback compiled
5268
+ // to a call on a nonexistent identifier, deferring the failure
5269
+ // to a confusing runtime ReferenceError instead of a clear
5270
+ // compile-time error naming the actual unimplemented function
5271
+ // (#257).
5272
+ throw new Error(
5273
+ `TS compiler: std.${fn} is not implemented (compileStdCall)`,
5274
+ );
5232
5275
  }
5233
5276
  }
5234
5277
  }
package/src/preamble.ts CHANGED
@@ -1114,6 +1114,26 @@ function __ball_map_entries(m: any): any {
1114
1114
  return Object.entries(m).map(([k, v]) => ({ key: k, value: v }));
1115
1115
  }
1116
1116
 
1117
+ // Shared guard for the REMAINING map_* base-function-call cases
1118
+ // (map_get/map_set/map_delete/map_merge/map_length/map_is_empty/
1119
+ // map_contains_key/map_contains_value/map_foreach) that used to route a
1120
+ // bare map[key], Object.keys/values(map), or key in map straight to the
1121
+ // receiver with no type check at all -- silently returning undefined,
1122
+ // no-opping, or checking array-index membership instead of throwing on a
1123
+ // non-Map (issue #55's silent-degradation class, same family as #218's
1124
+ // map_keys/map_values/map_entries). Returns the validated Map/plain-object
1125
+ // itself (not a boolean) so a call site can keep using it directly, e.g.
1126
+ // __ball_require_map(x, 'map_get')[key].
1127
+ function __ball_require_map(v: any, opName: string): any {
1128
+ if (v instanceof Map) return v;
1129
+ if (typeof v !== 'object' || v === null || Array.isArray(v) ||
1130
+ v instanceof BallDouble || v instanceof Set ||
1131
+ v instanceof Number || v instanceof String || v instanceof Boolean) {
1132
+ throw new Error('type \'' + __ball_to_string(v) + '\' is not a Map (' + opName + ')');
1133
+ }
1134
+ return v;
1135
+ }
1136
+
1117
1137
  // ── Protobuf Struct/Value compatibility ─────────────────────────
1118
1138
  //
1119
1139
  // Dart's protobuf runtime wraps google.protobuf.Struct as a class