@neocompose/cli 0.34.2 → 0.35.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.35.0] - 2026-08-19
4
+
5
+ ### Added
6
+
7
+ - P72 expression bodies: write `=> expr;` wherever a body is a single
8
+ expression — getter-only properties (`bool HasVisited => this.Count > 0;`),
9
+ an accessor inside a property block (`get => ...;`, `set(int value) => ...;`),
10
+ functions (`bool Ready(int level) => this.Level >= level;`), and lambdas
11
+ (`(x) => x * 2`). It is pure sugar: the desugared block compiles to the same
12
+ instruction stream, and diagnostics point at the authored expression. In a
13
+ void context the expression is a statement, so it must call or assign; an
14
+ expression whose value would be discarded is refused with a diagnostic that
15
+ names the block body as the fix.
16
+ - The `=>` reads as an expression body only where it directly follows a member
17
+ name or a parameter list's `)`, so a NeoFlow destination arrow after an `=`
18
+ initializer (`override Trigger Trigger = new(...) => Welcome;`) is unchanged.
19
+
20
+ ### Changed
21
+
22
+ - `neo pull` and `neo push` canonicalize a stored body to `=> expr;` when its
23
+ authored spelling is exactly one `return` statement that fits on one line;
24
+ every other body, and every setter, re-emits as a block. A push preserves the
25
+ form already on disk for members it did not otherwise rewrite, so authoring a
26
+ one-line getter in longhand stays byte-identical through a push.
27
+
28
+ ### Fixed
29
+
30
+ - VS Code highlighting no longer claims the identifier after every `=>` as a
31
+ NeoFlow destination. The destination scope now requires the NeoFlow shape —
32
+ an arrow after `)`, a single non-keyword identifier, then `;` — so calls,
33
+ `this.` chains, types, and `get`/`set` keywords inside expression bodies keep
34
+ their own scopes.
35
+
3
36
  ## [0.34.2] - 2026-08-19
4
37
 
5
38
  ### Fixed
package/dist/neo.mjs CHANGED
@@ -3293,7 +3293,13 @@ function insideLambdaBlock(tokens, catchBodyStart, offset) {
3293
3293
  continue;
3294
3294
  }
3295
3295
  const open = significant[index + 1];
3296
- if (open?.kind !== "punctuation" || open.text !== "{") continue;
3296
+ if (open === void 0) continue;
3297
+ if (open.kind !== "punctuation" || open.text !== "{") {
3298
+ if (open.start <= offset && offset <= lambdaExpressionEnd(significant, index + 1)) {
3299
+ return true;
3300
+ }
3301
+ continue;
3302
+ }
3297
3303
  const closeIndex = matchingCloseBraceIndex(significant, index + 1);
3298
3304
  const close = closeIndex === null ? void 0 : significant[closeIndex];
3299
3305
  if (open.end <= offset && (close === void 0 || offset <= close.start)) {
@@ -3302,6 +3308,23 @@ function insideLambdaBlock(tokens, catchBodyStart, offset) {
3302
3308
  }
3303
3309
  return false;
3304
3310
  }
3311
+ function lambdaExpressionEnd(tokens, firstIndex) {
3312
+ let depth = 0;
3313
+ let end = tokens[firstIndex]?.end ?? 0;
3314
+ for (let index = firstIndex; index < tokens.length; index++) {
3315
+ const token = tokens[index];
3316
+ if (!token) break;
3317
+ if (token.kind === "punctuation") {
3318
+ if (["(", "[", "{"].includes(token.text)) depth++;
3319
+ else if ([")", "]", "}"].includes(token.text)) {
3320
+ if (depth === 0) return end;
3321
+ depth--;
3322
+ } else if (token.text === ";" && depth === 0) return end;
3323
+ }
3324
+ end = token.end;
3325
+ }
3326
+ return end;
3327
+ }
3305
3328
  function matchingCloseBraceIndex(tokens, openIndex) {
3306
3329
  let depth = 0;
3307
3330
  for (let index = openIndex; index < tokens.length; index++) {
@@ -5076,14 +5099,16 @@ function parseUnits(tokens, pairs, kind, lexed) {
5076
5099
  }
5077
5100
  ];
5078
5101
  }
5102
+ const shorthand = propertyShorthandUnit(tokens, pairs, lexed);
5103
+ if (shorthand) return [shorthand];
5079
5104
  const units = [];
5080
5105
  const candidates = [];
5081
5106
  for (let index = 0; index < tokens.length; index++) {
5082
5107
  const token = tokens[index];
5083
5108
  if (!token) continue;
5084
5109
  if (token.kind === "identifier" && (token.text === "get" || token.text === "set") && isAccessorHeader(token, lexed)) {
5085
- const openIndex = accessorOpenBraceIndex(tokens, pairs, index);
5086
- if (openIndex !== null) candidates.push({ tokenIndex: index, openIndex });
5110
+ const open = accessorBodyOpen(tokens, pairs, index);
5111
+ if (open) candidates.push({ tokenIndex: index, ...open });
5087
5112
  }
5088
5113
  }
5089
5114
  for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) {
@@ -5093,6 +5118,18 @@ function parseUnits(tokens, pairs, kind, lexed) {
5093
5118
  const open = tokens[candidate.openIndex];
5094
5119
  if (!token || !open) continue;
5095
5120
  const nextCandidate = candidates[candidateIndex + 1];
5121
+ if (candidate.expressionBody) {
5122
+ const unit = expressionBodyUnit(
5123
+ tokens,
5124
+ pairs,
5125
+ lexed,
5126
+ token,
5127
+ candidate.openIndex,
5128
+ nextCandidate?.tokenIndex ?? tokens.length
5129
+ );
5130
+ if (unit) units.push(unit);
5131
+ continue;
5132
+ }
5096
5133
  const protectedCloseIndex = findProtectedRegionClose(
5097
5134
  tokens,
5098
5135
  candidate.openIndex,
@@ -5122,15 +5159,70 @@ function isAccessorHeader(token, lexed) {
5122
5159
  const line = lexed.source.lineText(token.range.start.line);
5123
5160
  return line.slice(0, token.range.start.character).trim().length === 0;
5124
5161
  }
5125
- function accessorOpenBraceIndex(tokens, pairs, accessorIndex) {
5126
- const next = tokens[accessorIndex + 1];
5127
- if (next?.kind !== "punctuation") return null;
5128
- if (next.text === "{") return accessorIndex + 1;
5129
- if (next.text !== "(") return null;
5130
- const closeParen = pairs.get(accessorIndex + 1);
5131
- if (closeParen === void 0) return null;
5132
- const openBrace = tokens[closeParen + 1];
5133
- return openBrace?.kind === "punctuation" && openBrace.text === "{" ? closeParen + 1 : null;
5162
+ function accessorBodyOpen(tokens, pairs, accessorIndex) {
5163
+ let openIndex = accessorIndex + 1;
5164
+ if (tokens[openIndex]?.text === "(") {
5165
+ const closeParen = pairs.get(openIndex);
5166
+ if (closeParen === void 0) return null;
5167
+ openIndex = closeParen + 1;
5168
+ }
5169
+ const open = tokens[openIndex];
5170
+ if (open?.kind === "punctuation" && open.text === "{") {
5171
+ return { openIndex, expressionBody: false };
5172
+ }
5173
+ if (open?.kind === "operator" && open.text === "=>") {
5174
+ return { openIndex, expressionBody: true };
5175
+ }
5176
+ return null;
5177
+ }
5178
+ function expressionBodyUnit(tokens, pairs, lexed, keyword, arrowIndex, limit) {
5179
+ const first = tokens[arrowIndex + 1];
5180
+ if (!first || first.text === ";") return null;
5181
+ const end = expressionBodyEnd(tokens, pairs, arrowIndex + 1, limit);
5182
+ if (end.bodyEnd <= first.start) return null;
5183
+ return {
5184
+ kind: keyword.text === "get" ? "getter" : "setter",
5185
+ range: lexed.source.range(keyword.start, end.unitEnd),
5186
+ bodyRange: lexed.source.range(first.start, end.bodyEnd),
5187
+ bodyStart: first.start,
5188
+ bodyEnd: end.bodyEnd,
5189
+ expressionBody: true
5190
+ };
5191
+ }
5192
+ function propertyShorthandUnit(tokens, pairs, lexed) {
5193
+ const arrow = tokens[0];
5194
+ if (arrow?.kind !== "operator" || arrow.text !== "=>") return null;
5195
+ const first = tokens[1];
5196
+ if (!first) return null;
5197
+ const end = expressionBodyEnd(tokens, pairs, 1, tokens.length);
5198
+ if (end.bodyEnd <= first.start) return null;
5199
+ return {
5200
+ kind: "getter",
5201
+ range: lexed.source.range(arrow.start, end.unitEnd),
5202
+ bodyRange: lexed.source.range(first.start, end.bodyEnd),
5203
+ bodyStart: first.start,
5204
+ bodyEnd: end.bodyEnd,
5205
+ expressionBody: true
5206
+ };
5207
+ }
5208
+ function expressionBodyEnd(tokens, pairs, firstIndex, limit) {
5209
+ let last = tokens[firstIndex];
5210
+ for (let cursor = firstIndex; cursor < limit; cursor++) {
5211
+ const token = tokens[cursor];
5212
+ if (!token) break;
5213
+ if (token.text === ";") {
5214
+ return { bodyEnd: last.end, unitEnd: token.end };
5215
+ }
5216
+ if (token.text === "}") return { bodyEnd: last.end, unitEnd: last.end };
5217
+ const close = OPEN_TO_CLOSE.has(token.text) ? pairs.get(cursor) : void 0;
5218
+ if (close !== void 0 && close < limit) {
5219
+ cursor = close;
5220
+ last = tokens[close] ?? last;
5221
+ continue;
5222
+ }
5223
+ last = token;
5224
+ }
5225
+ return { bodyEnd: last.end, unitEnd: last.end };
5134
5226
  }
5135
5227
  function findProtectedRegionClose(tokens, openIndex, indentation, limit) {
5136
5228
  for (let index = openIndex + 1; index < limit; index++) {
@@ -5386,8 +5478,8 @@ function validatePropertyUnits(units, diagnostics, lexed) {
5386
5478
  severity: "error",
5387
5479
  source: "neoscript",
5388
5480
  code: "missing-getter",
5389
- message: "A NeoScript property sidecar must declare one `get { ... }` unit.",
5390
- suggestions: ["Add `get { return ...; }`."]
5481
+ message: "A NeoScript property sidecar must declare one `get { ... }` or `get => ...;` unit.",
5482
+ suggestions: ["Add `get => ...;` or `get { return ...; }`."]
5391
5483
  });
5392
5484
  }
5393
5485
  if (getters.length > 1) duplicateUnitDiagnostic("get", getters, diagnostics);
@@ -5933,6 +6025,60 @@ var init_strict_compile_error = __esm({
5933
6025
  }
5934
6026
  });
5935
6027
 
6028
+ // ../packages/neoscript-language/src/strict-ast.ts
6029
+ function expressionBodyStatements(body, voidContext) {
6030
+ const pos = body.start;
6031
+ if (body.assignment) {
6032
+ return [
6033
+ {
6034
+ kind: "assign",
6035
+ target: body.expr,
6036
+ op: body.assignment.op,
6037
+ value: body.assignment.value,
6038
+ pos
6039
+ }
6040
+ ];
6041
+ }
6042
+ if (voidContext) {
6043
+ return [{ kind: "exprStmt", expr: body.expr, pos, expressionBody: true }];
6044
+ }
6045
+ return [{ kind: "return", expr: body.expr, pos }];
6046
+ }
6047
+ function collectReturnStatements(statements) {
6048
+ const result = [];
6049
+ for (const statement of statements) {
6050
+ if (statement.kind === "return") result.push(statement);
6051
+ if (statement.kind === "if") {
6052
+ for (const branch of statement.branches) {
6053
+ result.push(...collectReturnStatements(branch.body));
6054
+ }
6055
+ if (statement.elseBody) {
6056
+ result.push(...collectReturnStatements(statement.elseBody));
6057
+ }
6058
+ }
6059
+ if (statement.kind === "for" || statement.kind === "forEach") {
6060
+ result.push(...collectReturnStatements(statement.body));
6061
+ }
6062
+ if (statement.kind === "switch") {
6063
+ for (const section of statement.sections) {
6064
+ result.push(...collectReturnStatements(section.body));
6065
+ }
6066
+ }
6067
+ if (statement.kind === "try") {
6068
+ result.push(...collectReturnStatements(statement.body));
6069
+ for (const clause of statement.catches) {
6070
+ result.push(...collectReturnStatements(clause.body));
6071
+ }
6072
+ }
6073
+ }
6074
+ return result;
6075
+ }
6076
+ var init_strict_ast = __esm({
6077
+ "../packages/neoscript-language/src/strict-ast.ts"() {
6078
+ "use strict";
6079
+ }
6080
+ });
6081
+
5936
6082
  // ../packages/neoscript-language/src/project-source-identifiers.ts
5937
6083
  function isValidNeoProjectIdentifier(name) {
5938
6084
  return PROJECT_IDENTIFIER_PATTERN.test(name) && !PROJECT_RESERVED_NAMES.has(name);
@@ -6486,6 +6632,17 @@ function parseFunctionBody(source) {
6486
6632
  pos: tokens[0]?.pos ?? { line: 1, column: 1 }
6487
6633
  };
6488
6634
  }
6635
+ function parseExpressionBody(source) {
6636
+ const tokens = lex2(source);
6637
+ const parser = new Parser(tokens);
6638
+ const expressionBody = parser.parseExpressionBodyTail();
6639
+ parser.expect("eof");
6640
+ return {
6641
+ body: expressionBodyStatements(expressionBody, false),
6642
+ pos: expressionBody.start,
6643
+ expressionBody
6644
+ };
6645
+ }
6489
6646
  function parseExpression(source) {
6490
6647
  const parser = new Parser(lex2(source));
6491
6648
  const expression = parser.parseExpr();
@@ -6535,6 +6692,7 @@ var Parser;
6535
6692
  var init_strict_parser = __esm({
6536
6693
  "../packages/neoscript-language/src/strict-parser.ts"() {
6537
6694
  "use strict";
6695
+ init_strict_ast();
6538
6696
  init_strict_compile_error();
6539
6697
  init_language_spec();
6540
6698
  init_project_source_identifiers();
@@ -7651,6 +7809,16 @@ var init_strict_parser = __esm({
7651
7809
  }
7652
7810
  this.expect("punct", ")");
7653
7811
  this.expect("op", "=>");
7812
+ if (!(this.peek().kind === "punct" && this.peek().text === "{")) {
7813
+ const expressionBody = this.parseExpressionBodyTail();
7814
+ return {
7815
+ kind: "lambda",
7816
+ params,
7817
+ body: expressionBodyStatements(expressionBody, false),
7818
+ pos: startPos,
7819
+ expressionBody
7820
+ };
7821
+ }
7654
7822
  this.expect("punct", "{");
7655
7823
  const body = this.parseStatementList(false);
7656
7824
  this.expect("punct", "}");
@@ -7661,6 +7829,46 @@ var init_strict_parser = __esm({
7661
7829
  pos: startPos
7662
7830
  };
7663
7831
  }
7832
+ /**
7833
+ * Parses the body of an expression-bodied lambda or member — everything
7834
+ * after `=>`, stopping before any terminator the caller owns.
7835
+ *
7836
+ * Assignment and increment operators are accepted here because NeoScript
7837
+ * models them as statements rather than expressions; that is what makes
7838
+ * `set(int value) => _field = value` expressible. The resolver rejects the
7839
+ * assignment form wherever the unit has to produce a value.
7840
+ */
7841
+ parseExpressionBodyTail() {
7842
+ const start = this.peek().pos;
7843
+ if (isIncrementOp(this.peek())) {
7844
+ const opTok = this.next();
7845
+ const target = this.parseExpr();
7846
+ return this.expressionBody(start, target, {
7847
+ op: opTok.text,
7848
+ value: null
7849
+ });
7850
+ }
7851
+ const expr = this.parseExpr();
7852
+ if (isIncrementOp(this.peek())) {
7853
+ const opTok = this.next();
7854
+ return this.expressionBody(start, expr, {
7855
+ op: opTok.text,
7856
+ value: null
7857
+ });
7858
+ }
7859
+ if (isAssignmentOp(this.peek())) {
7860
+ const opTok = this.next();
7861
+ return this.expressionBody(start, expr, {
7862
+ op: opTok.text,
7863
+ value: this.parseExpr()
7864
+ });
7865
+ }
7866
+ return this.expressionBody(start, expr, null);
7867
+ }
7868
+ /** Closes an expression body at the token the parser has not consumed. */
7869
+ expressionBody(start, expr, assignment) {
7870
+ return { expr, assignment, start, end: this.peek().pos };
7871
+ }
7664
7872
  /**
7665
7873
  * Parses a single lambda parameter. Supports:
7666
7874
  * - `name` (untyped — resolver infers from receiver)
@@ -7738,42 +7946,6 @@ var init_strict_parser = __esm({
7738
7946
  }
7739
7947
  });
7740
7948
 
7741
- // ../packages/neoscript-language/src/strict-ast.ts
7742
- function collectReturnStatements(statements) {
7743
- const result = [];
7744
- for (const statement of statements) {
7745
- if (statement.kind === "return") result.push(statement);
7746
- if (statement.kind === "if") {
7747
- for (const branch of statement.branches) {
7748
- result.push(...collectReturnStatements(branch.body));
7749
- }
7750
- if (statement.elseBody) {
7751
- result.push(...collectReturnStatements(statement.elseBody));
7752
- }
7753
- }
7754
- if (statement.kind === "for" || statement.kind === "forEach") {
7755
- result.push(...collectReturnStatements(statement.body));
7756
- }
7757
- if (statement.kind === "switch") {
7758
- for (const section of statement.sections) {
7759
- result.push(...collectReturnStatements(section.body));
7760
- }
7761
- }
7762
- if (statement.kind === "try") {
7763
- result.push(...collectReturnStatements(statement.body));
7764
- for (const clause of statement.catches) {
7765
- result.push(...collectReturnStatements(clause.body));
7766
- }
7767
- }
7768
- }
7769
- return result;
7770
- }
7771
- var init_strict_ast = __esm({
7772
- "../packages/neoscript-language/src/strict-ast.ts"() {
7773
- "use strict";
7774
- }
7775
- });
7776
-
7777
7949
  // ../packages/neoscript-language/src/declared-constructors.ts
7778
7950
  function neoScriptArgumentNameSetKey(names) {
7779
7951
  return [...names].map((name) => name.toLowerCase()).sort().join(",");
@@ -9095,6 +9267,9 @@ function typesOverlap(left, right, project) {
9095
9267
  function isUnknown(type) {
9096
9268
  return type.kind === "primitive" && type.name === "unknown";
9097
9269
  }
9270
+ function describeUnitKind(kind) {
9271
+ return kind === "function" ? "NSFunction" : kind;
9272
+ }
9098
9273
  function isVoid(type) {
9099
9274
  return type?.kind === "primitive" && type.name === "void";
9100
9275
  }
@@ -9769,7 +9944,12 @@ var init_strict_resolver = __esm({
9769
9944
  );
9770
9945
  this.expectedReturnStack.push(returnType);
9771
9946
  try {
9772
- const instructions = this.resolveStatements(body.body, scope);
9947
+ const statements = this.expressionBodyOrBlock(
9948
+ body,
9949
+ this.context.kind === "action" || this.context.kind === "setter" || this.context.kind === "constructor" || isVoid(this.context.returnType),
9950
+ describeUnitKind(this.context.kind)
9951
+ );
9952
+ const instructions = this.resolveStatements(statements, scope);
9773
9953
  if ((this.context.kind === "function" || this.context.kind === "getter" || this.context.kind === "condition" || this.context.kind === "initializer") && !isVoid(this.context.returnType) && !instructionsTerminate(instructions)) {
9774
9954
  throw new CompileError(
9775
9955
  `Not every reachable ${FALLTHROUGH_LABELS[this.context.kind]} path returns a value or throws.`,
@@ -9790,6 +9970,24 @@ var init_strict_resolver = __esm({
9790
9970
  this.expectedReturnStack.pop();
9791
9971
  }
9792
9972
  }
9973
+ /**
9974
+ * The statements to compile for a unit or lambda, re-deriving an
9975
+ * expression-bodied `=> expr` now that the target's void-ness is known.
9976
+ *
9977
+ * The parser cannot make this call: `=> DoThing()` is a returned value in a
9978
+ * `string` getter and a discarded statement in an action.
9979
+ */
9980
+ expressionBodyOrBlock(node, voidContext, unitLabel2) {
9981
+ const authored = node.expressionBody;
9982
+ if (!authored) return node.body;
9983
+ if (authored.assignment && !voidContext) {
9984
+ throw new CompileError(
9985
+ `An expression-bodied ${unitLabel2} has to produce a value, and an assignment does not; use a block body instead.`,
9986
+ authored.start
9987
+ );
9988
+ }
9989
+ return expressionBodyStatements(authored, voidContext);
9990
+ }
9793
9991
  defineValueAliases(scope) {
9794
9992
  const aliases = this.context.valueAliases ?? [];
9795
9993
  if (aliases.length === 0) return;
@@ -10424,7 +10622,7 @@ var init_strict_resolver = __esm({
10424
10622
  };
10425
10623
  }
10426
10624
  throw new CompileError(
10427
- "Bare expression statements aren't supported \u2014 every expression must contribute to a return / throw / variable initialiser.",
10625
+ statement.expressionBody ? "A void expression body has to call something; an expression whose value is discarded must move into a block body." : "Bare expression statements aren't supported \u2014 every expression must contribute to a return / throw / variable initialiser.",
10428
10626
  statement.pos
10429
10627
  );
10430
10628
  }
@@ -11427,8 +11625,11 @@ var init_strict_resolver = __esm({
11427
11625
  this.lambdaDepth++;
11428
11626
  this.expectedReturnStack.push(expected.returnType);
11429
11627
  try {
11430
- const instructions = this.resolveStatements(ast.body, scope);
11431
11628
  const returnsVoid = isVoid(expected.returnType);
11629
+ const instructions = this.resolveStatements(
11630
+ this.expressionBodyOrBlock(ast, returnsVoid, "delegate"),
11631
+ scope
11632
+ );
11432
11633
  if (!returnsVoid && !instructionsTerminate(instructions)) {
11433
11634
  throw new CompileError(
11434
11635
  "Not every reachable delegate path returns a value or throws.",
@@ -11486,6 +11687,13 @@ var init_strict_resolver = __esm({
11486
11687
  (token) => token.pos.line === ast.pos.line && token.pos.column === ast.pos.column
11487
11688
  );
11488
11689
  if (startIndex < 0) return void 0;
11690
+ const expressionBody = ast.expressionBody;
11691
+ if (expressionBody) {
11692
+ const start = sourceOffset(this.source, ast.pos);
11693
+ const end = sourceOffset(this.source, expressionBody.end);
11694
+ if (end <= start) return void 0;
11695
+ return this.source.slice(start, end).trimEnd();
11696
+ }
11489
11697
  const openIndex = tokens.findIndex(
11490
11698
  (token, index) => index > startIndex && token.kind === "punct" && token.text === "{"
11491
11699
  );
@@ -14497,7 +14705,10 @@ var init_strict_resolver = __esm({
14497
14705
  this.lambdaDepth++;
14498
14706
  this.expectedReturnStack.push(returnType);
14499
14707
  try {
14500
- const instructions = this.resolveStatements(ast.body, scope);
14708
+ const instructions = this.resolveStatements(
14709
+ this.expressionBodyOrBlock(ast, false, "lambda"),
14710
+ scope
14711
+ );
14501
14712
  if (!instructionsTerminate(instructions)) {
14502
14713
  throw new CompileError(
14503
14714
  "Not every reachable lambda path returns a value or throws.",
@@ -15490,14 +15701,15 @@ function compileNeoScriptStrict(document, context, options = {}) {
15490
15701
  (unit) => unit.kind === "function" || unit.kind === "body"
15491
15702
  );
15492
15703
  const expressionDocument = context.kind === "initializerExpression";
15493
- const compilableSource = expressionDocument ? `return
15704
+ const expressionBodyUnit2 = options.bodyMode === "expression";
15705
+ const compilableSource = expressionBodyUnit2 ? document.text : expressionDocument ? `return
15494
15706
  ${document.text};` : functionUnit ? isolateUnitBody(
15495
15707
  document.text,
15496
15708
  functionUnit.bodyStart,
15497
15709
  functionUnit.bodyEnd
15498
15710
  ) : document.text;
15499
15711
  const resolverContext = expressionDocument ? { ...context, kind: "initializer" } : context;
15500
- const ast = parseFunctionBody(compilableSource);
15712
+ const ast = expressionBodyUnit2 ? parseExpressionBody(compilableSource) : parseFunctionBody(compilableSource);
15501
15713
  const projectIndex = options.projectIndex ?? createProjectIndex(resolverContext.project);
15502
15714
  const emitted = new StrictNeoScriptResolver(
15503
15715
  resolverContext,
@@ -15556,7 +15768,7 @@ function compileProperty(document, context, units, projectIndex) {
15556
15768
  const getterUnit = units.find((unit) => unit.kind === "getter");
15557
15769
  if (!getterUnit) {
15558
15770
  throw new CompileError(
15559
- "A NeoScript property sidecar must declare one `get { ... }` unit.",
15771
+ "A NeoScript property sidecar must declare one `get { ... }` or `get => ...;` unit.",
15560
15772
  { line: 1, column: 1 },
15561
15773
  "getter"
15562
15774
  );
@@ -15584,7 +15796,7 @@ function compileProperty(document, context, units, projectIndex) {
15584
15796
  function compileUnit(source, unit, context, label, projectIndex) {
15585
15797
  const isolated = isolateUnitBody(source, unit.bodyStart, unit.bodyEnd);
15586
15798
  try {
15587
- const ast = parseFunctionBody(isolated);
15799
+ const ast = unit.expressionBody ? parseExpressionBody(isolated) : parseFunctionBody(isolated);
15588
15800
  const indexedProject = projectIndex ?? createProjectIndex(context.project);
15589
15801
  return attachDependencyManifest(
15590
15802
  new StrictNeoScriptResolver(context, indexedProject, isolated).compile(
@@ -17950,8 +18162,8 @@ var init_project_source_parser = __esm({
17950
18162
  docsText
17951
18163
  );
17952
18164
  }
17953
- if (this.at("{")) {
17954
- const body = this.captureBody();
18165
+ if (this.at("{") || this.at("=>")) {
18166
+ const body = this.at("{") ? this.captureBody() : this.captureExpressionBody("property");
17955
18167
  this.reportNestedTypes(body);
17956
18168
  const property2 = {
17957
18169
  kind: "property",
@@ -18002,7 +18214,7 @@ var init_project_source_parser = __esm({
18002
18214
  if (this.eat(";")) {
18003
18215
  body = void 0;
18004
18216
  } else {
18005
- body = this.captureBody();
18217
+ body = this.at("=>") ? this.captureExpressionBody("method") : this.captureBody();
18006
18218
  this.reportNestedTypes(body);
18007
18219
  }
18008
18220
  return {
@@ -18340,6 +18552,36 @@ var init_project_source_parser = __esm({
18340
18552
  range: rangeFrom(open, close)
18341
18553
  };
18342
18554
  }
18555
+ /**
18556
+ * P72 §2. `=> expr;` in member position, reached only when the arrow follows
18557
+ * the member name or the parameter list's `)`. An arrow that follows an `=`
18558
+ * initializer never gets here, so a NeoFlow destination keeps its meaning.
18559
+ */
18560
+ captureExpressionBody(label) {
18561
+ const arrow = this.expect("=>");
18562
+ const expression = this.captureUntilTopLevel(/* @__PURE__ */ new Set([";"]), true);
18563
+ if (expression.text.trim().length === 0) {
18564
+ throw this.failure(
18565
+ "empty-expression-body",
18566
+ `An expression-bodied ${label} needs an expression after \`=>\`.`,
18567
+ this.peek()
18568
+ );
18569
+ }
18570
+ const close = this.previous();
18571
+ if (!this.at(";")) {
18572
+ throw this.failure(
18573
+ "unterminated-expression-body",
18574
+ `An expression-bodied ${label} must end with \`;\`.`,
18575
+ this.peek()
18576
+ );
18577
+ }
18578
+ this.next();
18579
+ return {
18580
+ text: this.source.text.slice(arrow.start, close.end),
18581
+ range: rangeFrom(arrow, close),
18582
+ expression
18583
+ };
18584
+ }
18343
18585
  captureUntilTopLevel(terminators, allowImplicitBodyEnd) {
18344
18586
  const start = this.peek();
18345
18587
  const startCursor = this.cursor;
@@ -24771,6 +25013,10 @@ function findProjectSourceAccessorBody(source, keyword, containingRange) {
24771
25013
  const body = source.slice(bodyStart, bodyEnd);
24772
25014
  const local = new SourceText(body);
24773
25015
  const tokens = lex2(body);
25016
+ if (tokens[0]?.text === "=>") {
25017
+ if (keyword !== "get") return null;
25018
+ return expressionAccessor(source, full, local, tokens, bodyStart, 1);
25019
+ }
24774
25020
  let depth = 0;
24775
25021
  for (let index = 0; index < tokens.length; index++) {
24776
25022
  const token = tokens[index];
@@ -24797,6 +25043,16 @@ function findProjectSourceAccessorBody(source, keyword, containingRange) {
24797
25043
  if (parameterDepth !== 0) return null;
24798
25044
  }
24799
25045
  const open = tokens[openIndex];
25046
+ if (open?.text === "=>") {
25047
+ return expressionAccessor(
25048
+ source,
25049
+ full,
25050
+ local,
25051
+ tokens,
25052
+ bodyStart,
25053
+ openIndex + 1
25054
+ );
25055
+ }
24800
25056
  if (open?.text !== "{") return null;
24801
25057
  let accessorDepth = 1;
24802
25058
  for (let cursor = openIndex + 1; cursor < tokens.length; cursor++) {
@@ -24808,13 +25064,52 @@ function findProjectSourceAccessorBody(source, keyword, containingRange) {
24808
25064
  const end = bodyStart + sourceOffset2(local, candidate.pos);
24809
25065
  return {
24810
25066
  range: full.range(start, end),
24811
- source: source.slice(start, end)
25067
+ source: source.slice(start, end),
25068
+ form: "block"
24812
25069
  };
24813
25070
  }
24814
25071
  return null;
24815
25072
  }
24816
25073
  return null;
24817
25074
  }
25075
+ function expressionAccessor(source, full, local, tokens, bodyStart, firstIndex) {
25076
+ const first = tokens[firstIndex];
25077
+ if (!first) return null;
25078
+ const start = bodyStart + sourceOffset2(local, first.pos);
25079
+ let end = bodyStart + local.text.length;
25080
+ let depth = 0;
25081
+ for (let cursor = firstIndex; cursor < tokens.length; cursor++) {
25082
+ const token = tokens[cursor];
25083
+ if (token.kind !== "punct") continue;
25084
+ if (token.text === "{" || token.text === "(" || token.text === "[") {
25085
+ depth++;
25086
+ continue;
25087
+ }
25088
+ if (token.text === ")" || token.text === "]") {
25089
+ depth--;
25090
+ continue;
25091
+ }
25092
+ if (token.text === "}") {
25093
+ if (depth === 0) {
25094
+ end = bodyStart + sourceOffset2(local, token.pos);
25095
+ break;
25096
+ }
25097
+ depth--;
25098
+ continue;
25099
+ }
25100
+ if (depth === 0 && token.text === ";") {
25101
+ end = bodyStart + sourceOffset2(local, token.pos);
25102
+ break;
25103
+ }
25104
+ }
25105
+ const trimmed = source.slice(start, end).trimEnd();
25106
+ if (trimmed.length === 0) return null;
25107
+ return {
25108
+ range: full.range(start, start + trimmed.length),
25109
+ source: trimmed,
25110
+ form: "expression"
25111
+ };
25112
+ }
24818
25113
  function sourceOffset2(source, position) {
24819
25114
  return source.offsetAt({
24820
25115
  line: Math.max(0, position.line - 1),
@@ -24939,6 +25234,7 @@ function compileProjectSourceBodies(documents) {
24939
25234
  staticMember
24940
25235
  };
24941
25236
  if (member.kind === "function" && member.body) {
25237
+ const expressionBody = member.body.expression;
24942
25238
  compileBody({
24943
25239
  uri,
24944
25240
  document,
@@ -24947,7 +25243,8 @@ function compileProjectSourceBodies(documents) {
24947
25243
  declarationRange: member.nameRange,
24948
25244
  memberId: info.symbol.id,
24949
25245
  unit: "function",
24950
- range: innerBodyRange(document.sourceText, member.body.range),
25246
+ range: expressionBody?.range ?? innerBodyRange(document.sourceText, member.body.range),
25247
+ ...expressionBody ? { bodyMode: "expression" } : {},
24951
25248
  context: {
24952
25249
  ...baseContext2,
24953
25250
  kind: "function",
@@ -24970,12 +25267,12 @@ function compileProjectSourceBodies(documents) {
24970
25267
  }
24971
25268
  if (member.kind !== "property") continue;
24972
25269
  for (const unit of ["getter", "setter"]) {
24973
- const range2 = findProjectSourceAccessorBody(
25270
+ const accessor = findProjectSourceAccessorBody(
24974
25271
  document.sourceText,
24975
25272
  unit === "getter" ? "get" : "set",
24976
25273
  member.body.range
24977
- )?.range;
24978
- if (!range2) continue;
25274
+ );
25275
+ if (!accessor) continue;
24979
25276
  compileBody({
24980
25277
  uri,
24981
25278
  document,
@@ -24984,7 +25281,8 @@ function compileProjectSourceBodies(documents) {
24984
25281
  declarationRange: member.nameRange,
24985
25282
  memberId: info.symbol.id,
24986
25283
  unit,
24987
- range: range2,
25284
+ range: accessor.range,
25285
+ ...accessor.form === "expression" ? { bodyMode: "expression" } : {},
24988
25286
  context: { ...baseContext2, kind: unit },
24989
25287
  projectIndex,
24990
25288
  bodies,
@@ -25017,7 +25315,10 @@ function compileBody(args) {
25017
25315
  text: maskOutside(args.document.sourceText, start, end)
25018
25316
  },
25019
25317
  args.context,
25020
- { projectIndex: args.projectIndex }
25318
+ {
25319
+ projectIndex: args.projectIndex,
25320
+ ...args.bodyMode ? { bodyMode: args.bodyMode } : {}
25321
+ }
25021
25322
  );
25022
25323
  for (const diagnostic of result.diagnostics) {
25023
25324
  args.diagnostics.push({
@@ -26614,7 +26915,7 @@ function validateClassMemberModifiers(ref, environment, diagnostics) {
26614
26915
  `Abstract member '${member.name}' cannot declare a default initializer.`
26615
26916
  );
26616
26917
  }
26617
- if (modifiers.has("abstract") && member.kind === "property" && /\b(?:get|set)\s*\{/.test(member.body.text)) {
26918
+ if (modifiers.has("abstract") && member.kind === "property" && (member.body.expression !== void 0 || /\b(?:get|set)\s*\{/.test(member.body.text))) {
26618
26919
  diagnose(
26619
26920
  diagnostics,
26620
26921
  ref.uri,
@@ -27017,6 +27318,7 @@ function valueAccessors(member, environment) {
27017
27318
  };
27018
27319
  }
27019
27320
  if (member.kind === "function") return { get: false, set: false };
27321
+ if (member.body.expression) return { get: true, set: false };
27020
27322
  return {
27021
27323
  get: /\bget\b/.test(member.body.text),
27022
27324
  set: /\bset\b/.test(member.body.text)
@@ -33296,9 +33598,10 @@ function scriptDefinitionLocations(member, environment, field) {
33296
33598
  const firstNonWhitespace = sourceText.search(/\S/);
33297
33599
  const startOffset = headerMember?.[1] === void 0 ? Math.max(firstNonWhitespace, 0) : sourceText.indexOf(headerMember[1]);
33298
33600
  const start = positionAt(sourceText, startOffset);
33299
- const tokenLength = headerMember?.[1] !== void 0 ? headerMember[1].length : kind === "property" && sourceText.slice(startOffset).startsWith("get") ? 3 : Math.max(
33300
- /^[A-Za-z_][A-Za-z0-9_]*/.exec(sourceText.slice(startOffset))?.[0].length ?? 1,
33301
- 1
33601
+ const tokenLength = definitionTokenLength(
33602
+ sourceText.slice(startOffset),
33603
+ kind,
33604
+ headerMember?.[1]
33302
33605
  );
33303
33606
  return {
33304
33607
  definitionLocations: [
@@ -33312,6 +33615,12 @@ function scriptDefinitionLocations(member, environment, field) {
33312
33615
  ]
33313
33616
  };
33314
33617
  }
33618
+ function definitionTokenLength(tail, kind, headerName) {
33619
+ if (headerName !== void 0) return headerName.length;
33620
+ if (kind === "property" && tail.startsWith("get")) return 3;
33621
+ if (tail.startsWith("=>")) return 2;
33622
+ return Math.max(/^[A-Za-z_][A-Za-z0-9_]*/.exec(tail)?.[0].length ?? 1, 1);
33623
+ }
33315
33624
  function positionAt(text, offset) {
33316
33625
  let line = 0;
33317
33626
  let character = 0;
@@ -50964,6 +51273,7 @@ function emitProjectSourcesV4(manifest, options = {}) {
50964
51273
  fileSymbols: options.fileSymbols ?? /* @__PURE__ */ new Map(),
50965
51274
  collectionValuePaths: options.collectionValuePaths ?? /* @__PURE__ */ new Map(),
50966
51275
  variantPathsById: buildVariantPathsByIdV4(manifest),
51276
+ formAlternates: [],
50967
51277
  templateNames: new Map(
50968
51278
  [...manifest.textureTemplates, ...manifest.audioTemplates].map(
50969
51279
  (template) => [template.id, template.name]
@@ -50971,6 +51281,7 @@ function emitProjectSourcesV4(manifest, options = {}) {
50971
51281
  )
50972
51282
  };
50973
51283
  const files = [];
51284
+ const formAlternates = [];
50974
51285
  const localizationStatusNames = new Map(
50975
51286
  manifest.localizationStatuses.map((entry) => [
50976
51287
  entry.id,
@@ -50982,9 +51293,14 @@ function emitProjectSourcesV4(manifest, options = {}) {
50982
51293
  const relationIds = manifest.internalRecordRelations.filter(
50983
51294
  (relation) => relation.sourceRecordKind === "class" && relation.sourceRecordId === schemaClass2.id && specializedRelationProperty(relation.relationKind) !== null
50984
51295
  ).map((relation) => relation.id);
51296
+ const path = `Classes/${schemaClass2.name}.neo`;
51297
+ const content = emitClass(context, schemaClass2, ownMemberIds);
51298
+ formAlternates.push(
51299
+ ...context.formAlternates.splice(0).map((draft) => ({ path, ...draft }))
51300
+ );
50985
51301
  files.push({
50986
- path: `Classes/${schemaClass2.name}.neo`,
50987
- content: emitClass(context, schemaClass2, ownMemberIds),
51302
+ path,
51303
+ content,
50988
51304
  recordKeys: [
50989
51305
  `class:${schemaClass2.id}`,
50990
51306
  ...(schemaClass2.constructorIds ?? []).map((id2) => `constructor:${id2}`),
@@ -51067,7 +51383,7 @@ function emitProjectSourcesV4(manifest, options = {}) {
51067
51383
  for (const file of files) {
51068
51384
  for (const key of file.recordKeys) recordFiles.set(key, file.path);
51069
51385
  }
51070
- return { files, recordFiles };
51386
+ return { files, recordFiles, formAlternates };
51071
51387
  }
51072
51388
  function emitGenericRelations(context, relations, endpointExpressions) {
51073
51389
  const ordered = [...relations].sort(
@@ -51197,10 +51513,16 @@ ${schemaClass2.genericParameters.map((parameter4) => {
51197
51513
  const header = `${modifier}class ${schemaClass2.name}${generics}${headerParameters}${bases.length ? ` : ${bases.join(", ")}` : ""} {`;
51198
51514
  const rendered = memberIds.map((memberId) => {
51199
51515
  const member = required(context.members, memberId, "member");
51200
- return {
51201
- member,
51202
- source: indentNeoSourceNonEmptyLines(emitMember(context, member, schemaClass2.id), 2)
51203
- };
51516
+ const alternateIndex = context.formAlternates.length;
51517
+ const source = indentNeoSourceNonEmptyLines(emitMember(context, member, schemaClass2.id), 2);
51518
+ for (let at = alternateIndex; at < context.formAlternates.length; at++) {
51519
+ const alternate = context.formAlternates[at];
51520
+ context.formAlternates[at] = {
51521
+ shorthand: indentNeoSourceNonEmptyLines(alternate.shorthand, 2),
51522
+ block: indentNeoSourceNonEmptyLines(alternate.block, 2)
51523
+ };
51524
+ }
51525
+ return { member, source };
51204
51526
  });
51205
51527
  const constructorIds = schemaClass2.constructorIds ?? [];
51206
51528
  const sources = rendered.map((entry) => entry.source);
@@ -51431,25 +51753,44 @@ function emitMember(context, member, enclosingClassId) {
51431
51753
  if (member.kind === "computed") {
51432
51754
  const getter = member.script?.getterSource?.trim();
51433
51755
  const setter = member.script?.setterSource?.trim();
51434
- const body = getter ? ` {
51435
- get {
51436
- ${indentNeoSourceNonEmptyLines(getter, 4)}
51437
- }${setter ? `
51756
+ const head = `${annotations.join("\n")}
51757
+ ${prefix}${renderType(context, member.returnType)} ${member.name}`;
51758
+ if (!getter) return `${head} { get; }`;
51759
+ const setterBlock = setter ? `
51438
51760
 
51439
51761
  set {
51440
51762
  ${indentNeoSourceNonEmptyLines(setter, 4)}
51441
- }` : ""}
51442
- }` : " { get; }";
51443
- return `${annotations.join("\n")}
51444
- ${prefix}${renderType(context, member.returnType)} ${member.name}${body}`;
51763
+ }` : "";
51764
+ const block = `${head} {
51765
+ get {
51766
+ ${indentNeoSourceNonEmptyLines(getter, 4)}
51767
+ }${setterBlock}
51768
+ }`;
51769
+ const expression = expressionBodyExpression(getter);
51770
+ if (expression === null) return block;
51771
+ const shorthand = setter ? `${head} {
51772
+ get => ${expression};${setterBlock}
51773
+ }` : `${head} => ${expression};`;
51774
+ context.formAlternates.push({ shorthand, block });
51775
+ return shorthand;
51445
51776
  }
51446
51777
  if (member.kind === "function" || member.kind === "scriptFunction") {
51447
51778
  const signature = `${prefix}${member.deferred ? "async " : ""}${renderReturnType(context, member.returnType)} ${member.name}(${member.arguments.map((argument2) => renderParameter(context, argument2)).join(", ")})`;
51448
51779
  const tracked = member.kind === "scriptFunction" ? member.script?.sourceText ?? uiFunctionSource(context, member) : null;
51449
51780
  const body = tracked === null ? null : emitFunctionBody(tracked);
51450
51781
  const abstractContract = member.modifier === "abstract" || member.modifier === "abstractOverride";
51451
- return `${annotations.join("\n")}
51452
- ${body !== null ? `${signature} ${body}` : abstractContract ? `${signature};` : `native ${signature};`}`;
51782
+ if (body === null) {
51783
+ return `${annotations.join("\n")}
51784
+ ${abstractContract ? `${signature};` : `native ${signature};`}`;
51785
+ }
51786
+ const block = `${annotations.join("\n")}
51787
+ ${signature} ${body}`;
51788
+ const expression = expressionBodyExpression(tracked ?? "");
51789
+ if (expression === null) return block;
51790
+ const shorthand = `${annotations.join("\n")}
51791
+ ${signature} => ${expression};`;
51792
+ context.formAlternates.push({ shorthand, block });
51793
+ return shorthand;
51453
51794
  }
51454
51795
  const type = renderMemberType(context, member, enclosingClassId);
51455
51796
  const initializer = initDefaultSource(member) ?? (member.isStatic ? context.staticInitializers.get(member.id) : void 0) ?? context.defaultInitializers.get(member.id) ?? renderDefault(context, member);
@@ -52320,6 +52661,22 @@ function systemAnnotation2(system) {
52320
52661
  ];
52321
52662
  return `@system(${args.join(", ")})`;
52322
52663
  }
52664
+ function expressionBodyExpression(sourceText) {
52665
+ const trimmed = sourceText.trim();
52666
+ if (trimmed.includes("\n")) return null;
52667
+ if (!trimmed.startsWith("return") || !trimmed.endsWith(";")) return null;
52668
+ let parsed;
52669
+ try {
52670
+ parsed = parseFunctionBody(trimmed);
52671
+ } catch {
52672
+ return null;
52673
+ }
52674
+ const statement = parsed.body[0];
52675
+ if (parsed.body.length !== 1 || statement?.kind !== "return") return null;
52676
+ if (statement.expr === null) return null;
52677
+ const expression = trimmed.slice("return".length, -1).trim();
52678
+ return expression.length === 0 ? null : expression;
52679
+ }
52323
52680
  function emitFunctionBody(sourceText) {
52324
52681
  const code = sourceText.trim();
52325
52682
  if (code.length === 0) return "{\n}";
@@ -53772,7 +54129,7 @@ function parseDialogueGroupFunctions(source) {
53772
54129
  return {
53773
54130
  id: annotationId(member.annotations),
53774
54131
  name: member.name,
53775
- code: member.body === void 0 ? null : innerSourceBody(member.body.text)
54132
+ code: member.body === void 0 ? null : dialogueGroupCode(member.body)
53776
54133
  };
53777
54134
  });
53778
54135
  }
@@ -53825,8 +54182,9 @@ function annotationId(annotations) {
53825
54182
  }
53826
54183
  return parsed;
53827
54184
  }
53828
- function innerSourceBody(body) {
53829
- const trimmed = body.trim();
54185
+ function dialogueGroupCode(body) {
54186
+ if (body.expression) return `return ${body.expression.text.trim()};`;
54187
+ const trimmed = body.text.trim();
53830
54188
  return trimmed.slice(1, -1).trim();
53831
54189
  }
53832
54190
  function resolveOptionalReference(expression, symbols) {
@@ -57817,7 +58175,7 @@ function lowerMemberKind(context, ownerClass, declaration, id2, owner) {
57817
58175
  }
57818
58176
  if (declaration.body !== null) {
57819
58177
  const sourceText = preserveEquivalentCode(
57820
- innerBody2(declaration.body),
58178
+ functionBodySource(declaration.body, returnType.kind === "void"),
57821
58179
  base?.kind === "scriptFunction" ? base.script?.sourceText : null
57822
58180
  );
57823
58181
  const uiAction = logicMode === "UI" ? lowerUiFunctionAction(
@@ -59119,7 +59477,7 @@ function lowerExpressionValue(context, expression, declaredExpected, ownerClass,
59119
59477
  `A lambda at ${path} requires a NeoDelegate expected type.`
59120
59478
  );
59121
59479
  }
59122
- const code = sourceText === void 0 ? null : lambdaExpressionSource(sourceText, expression.pos);
59480
+ const code = sourceText === void 0 ? null : lambdaExpressionSource(sourceText, expression);
59123
59481
  if (code === null || code.length === 0) {
59124
59482
  throw new Error(`Delegate value ${path} has no recoverable source.`);
59125
59483
  }
@@ -59232,14 +59590,25 @@ function isStoredActionLiteral(type, expression) {
59232
59590
  while (current.kind === "annotated") current = current.expression;
59233
59591
  return current.kind === "litList";
59234
59592
  }
59235
- function lambdaExpressionSource(source, pos) {
59236
- let start = 0;
59593
+ function sourceOffsetAt(source, pos) {
59594
+ let offset = 0;
59237
59595
  for (let line = 1; line < pos.line; line += 1) {
59238
- const next = source.indexOf("\n", start);
59596
+ const next = source.indexOf("\n", offset);
59239
59597
  if (next < 0) return null;
59240
- start = next + 1;
59598
+ offset = next + 1;
59599
+ }
59600
+ return Math.min(offset + pos.column - 1, source.length);
59601
+ }
59602
+ function lambdaExpressionSource(source, lambda) {
59603
+ const start = sourceOffsetAt(source, lambda.pos);
59604
+ if (start === null) return null;
59605
+ const expressionBody = lambda.expressionBody;
59606
+ if (expressionBody) {
59607
+ const end = sourceOffsetAt(source, expressionBody.end);
59608
+ if (end === null || end <= start) return null;
59609
+ const slice = source.slice(start, end).trimEnd();
59610
+ return slice.length === 0 ? null : slice;
59241
59611
  }
59242
- start = Math.min(start + pos.column - 1, source.length);
59243
59612
  const arrow = source.indexOf("=>", start);
59244
59613
  if (arrow < 0) return null;
59245
59614
  const bodyStart = source.indexOf("{", arrow + 2);
@@ -59837,14 +60206,22 @@ function memberModifier3(modifiers, baseModifier) {
59837
60206
  function propertyAccessors(body) {
59838
60207
  const accessorBody = (keyword) => {
59839
60208
  const accessor = findProjectSourceAccessorBody(body, keyword);
59840
- return accessor === null ? null : normalizeBodySource(accessor.source);
60209
+ if (accessor === null) return null;
60210
+ const source = normalizeBodySource(accessor.source);
60211
+ if (accessor.form === "block") return source;
60212
+ return keyword === "get" ? `return ${source};` : `${source};`;
59841
60213
  };
59842
60214
  return {
59843
60215
  get: accessorBody("get"),
59844
60216
  set: accessorBody("set")
59845
60217
  };
59846
60218
  }
59847
- function innerBody2(body) {
60219
+ function functionBodySource(body, isVoid2) {
60220
+ const trimmed = body.trim();
60221
+ if (trimmed.startsWith("=>")) {
60222
+ const expression = normalizeBodySource(trimmed.slice(2));
60223
+ return isVoid2 ? `${expression};` : `return ${expression};`;
60224
+ }
59848
60225
  const start = body.indexOf("{");
59849
60226
  const end = body.lastIndexOf("}");
59850
60227
  if (start < 0 || end <= start) return normalizeBodySource(body);
@@ -103466,7 +103843,13 @@ ${errors.map(
103466
103843
  ).join("\n")}`
103467
103844
  );
103468
103845
  }
103469
- return { files, recordFiles, analysis, materializedConstructors };
103846
+ return {
103847
+ files,
103848
+ recordFiles,
103849
+ analysis,
103850
+ materializedConstructors,
103851
+ formAlternates: source.formAlternates
103852
+ };
103470
103853
  }
103471
103854
  function relationEndpointExpressions(records2) {
103472
103855
  const ownerByMemberId = /* @__PURE__ */ new Map();
@@ -112325,6 +112708,30 @@ function applyPushResult(workspace, result, pendingIdAssignments = /* @__PURE__
112325
112708
  rewriteFilesFromState(workspace, void 0, preservedSourceFiles);
112326
112709
  writeWorkspaceState(workspace.root, workspace.state);
112327
112710
  }
112711
+ function authoredMemberFormsByFile(root, alternates) {
112712
+ const authored = /* @__PURE__ */ new Map();
112713
+ const contents = /* @__PURE__ */ new Map();
112714
+ for (const alternate of alternates) {
112715
+ let existing = contents.get(alternate.path);
112716
+ if (existing === void 0) {
112717
+ const absolute = join15(root, alternate.path);
112718
+ existing = existsSync11(absolute) ? readFileSync15(absolute, "utf8") : null;
112719
+ contents.set(alternate.path, existing);
112720
+ }
112721
+ if (existing === null || !existing.includes(alternate.block)) continue;
112722
+ const bucket = authored.get(alternate.path);
112723
+ if (bucket) bucket.push(alternate);
112724
+ else authored.set(alternate.path, [alternate]);
112725
+ }
112726
+ return authored;
112727
+ }
112728
+ function restoreAuthoredMemberForms(content, alternates) {
112729
+ let result = content;
112730
+ for (const alternate of alternates) {
112731
+ result = result.split(alternate.shorthand).join(alternate.block);
112732
+ }
112733
+ return result;
112734
+ }
112328
112735
  function rewriteFilesFromState(workspace, records2 = new Map(
112329
112736
  Object.entries(workspace.state.records).map(([key, recordState]) => [
112330
112737
  key,
@@ -112338,9 +112745,16 @@ function rewriteFilesFromState(workspace, records2 = new Map(
112338
112745
  ])
112339
112746
  ), preservedSourceFiles = /* @__PURE__ */ new Map()) {
112340
112747
  const result = emitProjectDocumentFilesV4(records2);
112748
+ const authoredForms = authoredMemberFormsByFile(
112749
+ workspace.root,
112750
+ result.formAlternates
112751
+ );
112341
112752
  const files = result.files.map((file) => ({
112342
112753
  ...file,
112343
- content: preservedSourceFiles.get(file.path) ?? file.content
112754
+ content: preservedSourceFiles.get(file.path) ?? restoreAuthoredMemberForms(
112755
+ file.content,
112756
+ authoredForms.get(file.path) ?? []
112757
+ )
112344
112758
  }));
112345
112759
  const finalAnalysis = compileNeoProjectSources(
112346
112760
  files.map((file) => {
@@ -112487,7 +112901,7 @@ function materializeAssignedSchemaIdsInAuthoredSource(workspace, replacements, p
112487
112901
  if (source === void 0) continue;
112488
112902
  const edits = editsByUri.get(uri) ?? [];
112489
112903
  if (entry.identity.id === null) {
112490
- const start = sourceOffsetAt(
112904
+ const start = sourceOffsetAt2(
112491
112905
  source,
112492
112906
  entry.identity.source.range.start.line,
112493
112907
  entry.identity.source.range.start.character
@@ -112519,7 +112933,7 @@ function materializeAssignedSchemaIdsInAuthoredSource(workspace, replacements, p
112519
112933
  if (assignedId === void 0) continue;
112520
112934
  const source = inputByUri.get(site.uri);
112521
112935
  if (source === void 0) continue;
112522
- const declarationOffset = sourceOffsetAt(
112936
+ const declarationOffset = sourceOffsetAt2(
112523
112937
  source,
112524
112938
  site.declarationStart.line,
112525
112939
  site.declarationStart.character
@@ -112587,7 +113001,7 @@ function pendingIdentityUri(pendingId2) {
112587
113001
  return null;
112588
113002
  }
112589
113003
  }
112590
- function sourceOffsetAt(source, line, character) {
113004
+ function sourceOffsetAt2(source, line, character) {
112591
113005
  let offset = 0;
112592
113006
  for (let currentLine = 0; currentLine < line; currentLine += 1) {
112593
113007
  const newline = source.indexOf("\n", offset);
@@ -113181,7 +113595,7 @@ var init_registry2 = __esm({
113181
113595
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
113182
113596
  formatVersion: 3,
113183
113597
  contractVersion: "3.14",
113184
- cliVersion: "0.34.2",
113598
+ cliVersion: "0.35.0",
113185
113599
  projectFileUploadBatchSize: 32,
113186
113600
  documentRecords: {
113187
113601
  member: {
@@ -119783,7 +120197,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
119783
120197
  async function main() {
119784
120198
  const args = parseArgs(process.argv.slice(2));
119785
120199
  if (args.command === "--version") {
119786
- console.log("0.34.2");
120200
+ console.log("0.35.0");
119787
120201
  return;
119788
120202
  }
119789
120203
  if (args.command === null || args.command === "help" || args.command === "--help" || args.command === "-h") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neocompose/cli",
3
- "version": "0.34.2",
3
+ "version": "0.35.0",
4
4
  "description": "Neo Compose native project-source CLI with bidirectional sync.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,7 +9,7 @@ description: >-
9
9
  `@neocompose/cli` or `node cli/bin/neo.mjs` in the neo-compose repository.
10
10
  ---
11
11
 
12
- <!-- reviewed-through-cli: 0.34.2 -->
12
+ <!-- reviewed-through-cli: 0.35.0 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -47,9 +47,8 @@ Treat pull followed immediately by status or dry-run as a semantic no-op. If it
47
47
  reports project changes, stop and report a round-trip bug instead of pushing.
48
48
 
49
49
  The compiler reports file:line:column diagnostics and fails closed. Unknown
50
- non-null authorable server fields require a CLI contract update; never preserve
51
- them as opaque JSON or guessed syntax. An unknown top-level document field whose
52
- value is exactly JSON `null` is treated as absent.
50
+ authorable server fields require a CLI contract update; never preserve them as
51
+ opaque JSON or guessed syntax.
53
52
 
54
53
  ## Honor hard authoring invariants
55
54
 
@@ -45,9 +45,12 @@ NeoVariant<ExampleObject> Down = new(
45
45
  required: it is the only way to construct through the variant.
46
46
  - `apply` takes exactly one parameter of the target class and is void. Omit it
47
47
  for a variant that is only ever constructed.
48
- - Both are ordinary delegate closures: parenthesized parameter list, block
49
- body, no expression-bodied form. The server compiles them, so authored source
50
- carries only the code.
48
+ - Both are ordinary delegate closures: parenthesized parameter list, then
49
+ either a block body or an expression body. `initialize` returns, so
50
+ `() => new ExampleObject(.Down)` works; `apply` is void, so its expression
51
+ body must be a call or an assignment, as in
52
+ `(source) => source.FacingDir = .Down`. The server compiles them, so authored
53
+ source carries only the code.
51
54
 
52
55
  Folders are records too, declared as their own globals and assigned per
53
56
  variant. Nesting is spelled in the path with `/`; intermediate segments are
@@ -346,8 +349,11 @@ node bindings scoped to their node/children. Give each persisted condition use,
346
349
  action invocation, mutation, pause, option, and node its own owner-scoped ID.
347
350
 
348
351
  Use `=> Next` or a trailing block with `return Next;`; do not author a `to:`
349
- argument. Require destinations for triggers, options, and outcomes. Permit
350
- terminal text/actions to fall through. Keep all statements for
352
+ argument. A destination arrow always follows an `=` initializer, as in
353
+ `= new(...) => Welcome;`; an arrow that instead follows a member name or a
354
+ parameter list's `)` is a NeoScript expression body, so the two never collide.
355
+ Require destinations for triggers, options, and outcomes. Permit terminal
356
+ text/actions to fall through. Keep all statements for
351
357
  `Actions Empty = new();` inside its body.
352
358
 
353
359
  After editing, run `neo dialogue dryrun <dialogue-ref>`. Exit 1 means the graph
@@ -83,7 +83,7 @@ wrappers.
83
83
  The marker near the top of `SKILL.md` must exactly match the package version:
84
84
 
85
85
  ```html
86
- <!-- reviewed-through-cli: 0.34.2 -->
86
+ <!-- reviewed-through-cli: 0.35.0 -->
87
87
  ```
88
88
 
89
89
  The quoted version above is checked too, so this instruction cannot go stale
@@ -25,6 +25,41 @@ Declare a setter's value type explicitly with the canonical form
25
25
  `set(int value) { ... }`. Project analysis, candidate materialization, and
26
26
  focused script commands all use the same accessor extraction.
27
27
 
28
+ A body whose whole point is one expression may be written as an expression
29
+ body, `=> expr;`. It is pure sugar for the block form — same compiled
30
+ instructions, same diagnostics, same stored source — and is available on
31
+ getter-only properties, individual accessors, and functions:
32
+
33
+ ```neo
34
+ public bool HasVisited => this.VisitCount > 0;
35
+
36
+ public int Clamped {
37
+ get => Math.Clamp(this.Raw, 0, 10);
38
+ set(int value) { this.Raw = value; }
39
+ }
40
+
41
+ public bool Unlocked(int level) => this.Level >= level;
42
+ ```
43
+
44
+ The `=>` is only an expression body where it directly follows the member name
45
+ or the parameter list's `)`. An arrow after an `=` initializer is always a
46
+ NeoFlow destination, so `override Trigger Trigger = new(...) => Welcome;`
47
+ keeps its meaning.
48
+
49
+ In a void context — a `void` function, a setter, or a void delegate — `=> expr`
50
+ is an expression statement rather than a return, so the expression must do
51
+ something: a call such as `void Ping() => this.OnChanged();`, or an assignment
52
+ such as `set(int value) => this.Raw = value;`. An expression whose value would
53
+ simply be discarded is rejected with a diagnostic that names the block body as
54
+ the fix.
55
+
56
+ `neo push` and `neo pull` canonicalize a stored body to the shorthand when its
57
+ authored spelling is exactly one `return` statement that fits on one line;
58
+ anything else re-emits as a block. Setters always re-emit as blocks. A push
59
+ preserves the form you authored for members it did not otherwise rewrite, so
60
+ choosing longhand for a one-line getter is stable; a fresh `neo pull` writes
61
+ the canonical shorthand.
62
+
28
63
  Declare a bodyless NeoScript contract with `abstract`, for example
29
64
  `public abstract bool Equals(Item other);`. Reserve `native` for host-provided
30
65
  functions; concrete NeoScript overrides inherit the abstract signature.
@@ -55,6 +90,15 @@ so later changes to the outer binding do not alter the closure. Explicit lambda
55
90
  parameter annotations accept the same type grammar as declarations, including
56
91
  `decimal`, generic parameters, collections, arrays, and nullable types.
57
92
 
93
+ A lambda may use an expression body too: `(x) => x * 2` is the block body
94
+ `(x) => { return x * 2; }`. In a void delegate the expression is a statement,
95
+ so it must call or assign. A dictionary-literal body needs parentheses,
96
+ `() => ({ "a": 1 })`, because a bare `{` after the arrow always opens a block.
97
+
98
+ ```neo
99
+ List<Item> ready = this.Items.Where((item) => item.Count > 0);
100
+ ```
101
+
58
102
  Calling `Equals(other)` on a non-null generic value dynamically uses the
59
103
  runtime Class's one-argument `Equals` Function or NSFunction when it returns
60
104
  `bool`, including the effective override. Values without that member fall back
@@ -39,9 +39,7 @@ class Assets {
39
39
  };
40
40
 
41
41
  @id("home-getter-id")
42
- static Outpost Home {
43
- get { return Assets.Capitol; }
44
- }
42
+ static Outpost Home => Assets.Capitol;
45
43
  }
46
44
  ```
47
45