@ball-lang/compiler 1.68.4 → 1.68.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ball-lang/compiler",
3
- "version": "1.68.4",
3
+ "version": "1.68.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
@@ -2702,9 +2702,18 @@ function __isUnknownFnError(e: any): boolean {
2702
2702
  ctors.push(this.buildCtor(fn, mMeta, fieldNames, hasExtends));
2703
2703
  } else {
2704
2704
  // Named constructors are static factory methods that return a new
2705
- // instance. Build them by creating an instance from initializers.
2705
+ // instance. The class's own (non-static) field declarations go with
2706
+ // them: the instance is built with `Object.create`, which runs no
2707
+ // field initializer, so the builder has to seed each default itself
2708
+ // (#564).
2706
2709
  methods.push(
2707
- this.buildNamedCtor(fn, mMeta, fieldNames, tsName),
2710
+ this.buildNamedCtor(
2711
+ fn,
2712
+ mMeta,
2713
+ fieldNames,
2714
+ tsName,
2715
+ properties.filter((p) => !p.isStatic),
2716
+ ),
2708
2717
  );
2709
2718
  }
2710
2719
  } else if (mMeta["is_getter"] === true) {
@@ -2851,8 +2860,15 @@ function __isUnknownFnError(e: any): boolean {
2851
2860
  }
2852
2861
  }
2853
2862
  } else {
2863
+ // ONLY a `this.`-formal writes a constructor parameter into the field of
2864
+ // the same name. A plain parameter that merely happens to share a
2865
+ // field's name is an ordinary local and must never clobber that field's
2866
+ // initializer — `class Foo { int x = 5; Foo(int x) { print(x); } }`
2867
+ // leaves `x` at 5 in Dart (#539/#564, conformance 453). The old
2868
+ // `|| classFields.has(p.name)` disjunct is the byte-identical defect the
2869
+ // Dart engine carried until #563.
2854
2870
  for (const p of rawParams) {
2855
- if (p.isThis || classFields.has(p.name)) {
2871
+ if (p.isThis) {
2856
2872
  prologueParts.push(`this.${p.name} = ${sanitize(p.name)};`);
2857
2873
  }
2858
2874
  }
@@ -2921,54 +2937,63 @@ function __isUnknownFnError(e: any): boolean {
2921
2937
 
2922
2938
  /**
2923
2939
  * Builds a named constructor as a static factory method.
2924
- * Named constructors (e.g., Point.origin, Point.fromList) create instances
2925
- * from field initializers in metadata.
2940
+ *
2941
+ * Every named constructor that is not a Dart `factory` CONSTRUCTS a real
2942
+ * instance — whether it carries an initializer list (`Point.origin() : x =
2943
+ * 0`), `this.`-formals (`Box.of(this.value)`), a plain body
2944
+ * (`Bar.named(int x) { print(x); }`), or any combination. The instance comes
2945
+ * from `Object.create(C.prototype)` so no user constructor runs, which means
2946
+ * the class's inline field initializers do not run either: every declared
2947
+ * field is seeded with its own default FIRST (Dart's ordering — inline field
2948
+ * initializers, then the initializer list, then the body), and only then do
2949
+ * the constructor-specific writes land on top (#564).
2950
+ *
2951
+ * A `factory` named constructor is the one exception: by definition it
2952
+ * returns some other object, so its body runs as the static method's own
2953
+ * statements and its value is the method's result.
2926
2954
  */
2927
2955
  private buildNamedCtor(
2928
2956
  fn: FunctionDef,
2929
2957
  meta: Struct,
2930
2958
  classFields: Set<string>,
2931
2959
  className: string,
2960
+ classProperties: Array<{
2961
+ name: string;
2962
+ type: string;
2963
+ rawDartType: string;
2964
+ dartInitializer?: string;
2965
+ }> = [],
2932
2966
  ) {
2933
2967
  const params = extractParams(fn);
2934
2968
  const ctorParams = extractCtorParams(meta);
2935
2969
  const initializers = Array.isArray(meta["initializers"]) ? meta["initializers"] as any[] : [];
2936
2970
  // Build the body: resolve field initializers and create a new instance.
2937
2971
  const bodyParts: string[] = [];
2938
- const ctorArgs: string[] = [];
2939
- // Extract field initializers to build constructor arguments
2940
- for (const init of initializers) {
2941
- if (init?.kind === "field" && typeof init.name === "string") {
2942
- const valueStr = typeof init.value === "string" ? init.value : "null";
2943
- // Resolve the value: could be a param reference, literal, or expression
2944
- let resolvedValue: string;
2945
- if (params.includes(valueStr)) {
2946
- resolvedValue = sanitize(valueStr);
2947
- } else if (/^-?\d+(\.\d+)?$/.test(valueStr)) {
2948
- // Numeric literal — wrap in BallDouble if it has a decimal point
2949
- resolvedValue = valueStr.includes(".") ? `new BallDouble(${valueStr})` : valueStr;
2950
- } else if (valueStr.startsWith("'") || valueStr.startsWith('"')) {
2951
- resolvedValue = valueStr;
2952
- } else {
2953
- // Try to parse indexed access like "coords[0]"
2954
- const idxMatch = /^(\w+)\[(\d+)\]$/.exec(valueStr);
2955
- if (idxMatch && params.includes(idxMatch[1])) {
2956
- resolvedValue = `${sanitize(idxMatch[1])}[${idxMatch[2]}]`;
2957
- } else {
2958
- resolvedValue = valueStr;
2959
- }
2960
- }
2961
- ctorArgs.push(resolvedValue);
2962
- }
2963
- }
2964
- // Also check for is_this params — they pass directly to the constructor
2972
+ // Does the ctor have any field write of its own? Only WHETHER matters
2973
+ // here; each value is resolved where the assignment is emitted, by
2974
+ // `resolveInitializerValue`. (A `this.`-formal counts only when there is
2975
+ // no initializer list at all, preserving the historical branch selection
2976
+ // for the `Foo.named(this.x) : super(...)` shape, which still falls
2977
+ // through to `return new Foo()`.)
2965
2978
  const thisParams = ctorParams.filter(p => p.isThis);
2966
- if (ctorArgs.length === 0 && initializers.length === 0 && thisParams.length > 0) {
2967
- for (const p of thisParams) {
2968
- ctorArgs.push(sanitize(p.name));
2969
- }
2970
- }
2971
- if (ctorArgs.length > 0 || (initializers.length > 0 && ctorArgs.length > 0)) {
2979
+ const hasFieldInitializers = initializers.some(
2980
+ (i: any) => i?.kind === "field" && typeof i.name === "string",
2981
+ );
2982
+ const hasCtorFieldWrites =
2983
+ hasFieldInitializers || (initializers.length === 0 && thisParams.length > 0);
2984
+ // A Dart `factory` returns some other object, so it is the ONE named
2985
+ // constructor that must not synthesize an instance of its own class.
2986
+ const isFactory = meta["is_factory"] === true;
2987
+ // Everything else constructs: an initializer list / `this.`-formal, or a
2988
+ // plain body, or both. A body-only named constructor
2989
+ // (`Bar.named(int x) { print(x); }`) used to fall through to the
2990
+ // run-the-body-as-a-static-method branch below, so it created no instance
2991
+ // at all — `this` inside it was the CLASS and the method returned the
2992
+ // body's value, i.e. `undefined` for a `print` (#564, conformance
2993
+ // 453/454).
2994
+ const constructsInstance =
2995
+ hasCtorFieldWrites || (fn.body !== undefined && !isFactory);
2996
+ if (constructsInstance) {
2972
2997
  // Use Object.create to avoid calling the constructor (which might be a factory).
2973
2998
  // This directly instantiates with fields set.
2974
2999
  const assignments = initializers
@@ -2978,15 +3003,27 @@ function __isUnknownFnError(e: any): boolean {
2978
3003
  const thisAssignments = thisParams.map(p => `__inst.${p.name} = ${sanitize(p.name)};`);
2979
3004
  const allAssignments = [...assignments, ...thisAssignments];
2980
3005
  bodyParts.push(`const __inst = Object.create(${className}.prototype);`);
3006
+ // `Object.create` deliberately runs no constructor — which also means it
3007
+ // runs none of the class's inline field initializers. Seed every
3008
+ // declared field with the SAME default the class declaration emits,
3009
+ // before any constructor-specific write, exactly as Dart orders them
3010
+ // (inline initializers, then the initializer list, then the body).
3011
+ // Without this a field the named constructor never mentions stayed
3012
+ // `undefined` forever (#564: `Init.viaList`'s `w`, `Baz.bare`'s `v`).
3013
+ for (const p of classProperties) {
3014
+ const def = dartInitializerToTs(p.dartInitializer, p.type, p.rawDartType);
3015
+ if (def !== undefined) bodyParts.push(`__inst.${p.name} = ${def};`);
3016
+ }
2981
3017
  for (const a of allAssignments) bodyParts.push(a);
2982
- // A named constructor may have BOTH an initializer list / `this.`-params
2983
- // AND a body (`Countdown.pair(int s) : value = s { tail = …; }`, or
2984
- // `Countdown.from(this.value) { … }`). Dart runs the list first, then the
2985
- // body; the body used to be dropped entirely by this branch, so every
2986
- // field the body set stayed unset (conformance 436/438). `.call(__inst)`
2987
- // — a real `function`, not an arrow — gives the compiled body the
2988
- // instance as `this`, so its `this.<field>` writes land on `__inst`,
2989
- // while the constructor's parameters stay in scope by closure.
3018
+ // A named constructor may have an initializer list / `this.`-params, a
3019
+ // body, or both (`Countdown.pair(int s) : value = s { tail = …; }`,
3020
+ // `Countdown.from(this.value) { … }`, `Bar.named(int x) { print(x); }`).
3021
+ // Dart runs the list first, then the body; the body used to be dropped
3022
+ // entirely by this branch, so every field the body set stayed unset
3023
+ // (conformance 436/438). `.call(__inst)` — a real `function`, not an
3024
+ // arrow — gives the compiled body the instance as `this`, so its
3025
+ // `this.<field>` writes land on `__inst`, while the constructor's
3026
+ // parameters stay in scope by closure.
2990
3027
  if (fn.body) {
2991
3028
  const captured = this.withMethodContext(
2992
3029
  new Set(params),
@@ -3002,7 +3039,10 @@ function __isUnknownFnError(e: any): boolean {
3002
3039
  }
3003
3040
  bodyParts.push(`return __inst;`);
3004
3041
  } else if (fn.body) {
3005
- // Has an actual body — use it
3042
+ // A `factory` named constructor: its body IS the method whatever it
3043
+ // evaluates to is what the caller gets, and no instance of this class is
3044
+ // synthesized. This is the only body-bearing shape that reaches here
3045
+ // (every non-factory body constructs, above).
3006
3046
  const captured = this.withMethodContext(
3007
3047
  new Set(params),
3008
3048
  classFields,
@@ -3013,6 +3053,8 @@ function __isUnknownFnError(e: any): boolean {
3013
3053
  );
3014
3054
  bodyParts.push(captured);
3015
3055
  } else {
3056
+ // Nothing to run and nothing to assign: let the real constructor build
3057
+ // it (which runs the class's inline field initializers itself).
3016
3058
  bodyParts.push(`return new ${className}();`);
3017
3059
  }
3018
3060
  return {