@neocompose/cli 0.31.17 → 0.32.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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.32.0] - 2026-08-18
4
+
5
+ ### Added
6
+
7
+ - P69 Math builtins: call `Math.Min`, `Max`, `Clamp`, `Round`, `Floor`,
8
+ `Ceiling`, `Truncate`, `Abs`, `Sign`, and `Sqrt`, and read the `Math.PI` and
9
+ `Math.E` constants, from any NeoScript body. Results are inferred from the
10
+ argument types under the arithmetic join rule, `Round`/`Floor`/`Ceiling`/
11
+ `Truncate` return `int` on `int` and `float` input, and `Math` is now a
12
+ reserved name.
13
+
3
14
  ## [0.31.17] - 2026-08-18
4
15
 
5
16
  ### Fixed
package/dist/neo.mjs CHANGED
@@ -1308,6 +1308,20 @@ function structuredLeafFieldProperties(owner) {
1308
1308
  (field) => property(owner, field.name, field.type, field.documentation, true)
1309
1309
  );
1310
1310
  }
1311
+ function mathFunction(name, parameterNames, documentation, returnType = FLOAT_TYPE) {
1312
+ return {
1313
+ id: `${NEOSCRIPT_MATH_TYPE_ID}:function:${name}`,
1314
+ name,
1315
+ kind: "function",
1316
+ type: returnType,
1317
+ returnType,
1318
+ parameters: parameterNames.map(
1319
+ (parameterName) => parameter(parameterName, FLOAT_TYPE)
1320
+ ),
1321
+ documentation,
1322
+ static: true
1323
+ };
1324
+ }
1311
1325
  function vectorTypes() {
1312
1326
  const names = [
1313
1327
  "Vector2",
@@ -1322,7 +1336,7 @@ function vectorTypes() {
1322
1336
  members: structuredLeafFieldProperties(name)
1323
1337
  }));
1324
1338
  }
1325
- var NEO_PROJECT_IGNORED_DIRECTORY_NAMES, NEOSCRIPT_KEYWORDS, NEOSCRIPT_INFERRED_LOCAL_KEYWORD, NEOSCRIPT_CONSTRUCTOR_KEYWORD, NEOSCRIPT_PRIMITIVE_TYPES, NEOSCRIPT_BUILTIN_TYPES, NEO_VARIANT_TYPE_NAME, NEO_VARIANT_FOLDER_TYPE_NAME, NEO_LOOKUP_VARIANT_TYPE_NAME, NEO_LOOKUP_VARIANT_FOLDER_TYPE_NAME, NEO_VARIANT_SCOPE_NAME, NEO_VARIANT_RESERVED_NAME, NEO_VARIANT_FOLDER_PATH_SEPARATOR, NEOSCRIPT_OPERATORS, NEOSCRIPT_STATEMENT_SNIPPETS, STRING_TYPE, INT_TYPE, FLOAT_TYPE, BOOL_TYPE, IMAGE_REF_TYPE, SPRITE_INFO_TYPE, FLOAT_OPTIONAL_TYPE, NEOSCRIPT_STRUCTURED_LEAF_FIELDS, NEOSCRIPT_IMAGE_REGISTRY_TYPE, NEOSCRIPT_AUDIO_CLIP_REGISTRY_TYPE, NEOSCRIPT_PENDING_SYMBOL_ID_PREFIX, REGISTRY_ENTRY_TYPES, NEOSCRIPT_GLOBALS, NEOSCRIPT_SPRITE_INFO_EMPTY_MEMBER_ID, NEOSCRIPT_SPRITE_INFO_EMPTY_VALUE, NEOSCRIPT_BUILTIN_TYPE_SYMBOLS;
1339
+ var NEO_PROJECT_IGNORED_DIRECTORY_NAMES, NEOSCRIPT_KEYWORDS, NEOSCRIPT_INFERRED_LOCAL_KEYWORD, NEOSCRIPT_CONSTRUCTOR_KEYWORD, NEOSCRIPT_PRIMITIVE_TYPES, NEOSCRIPT_BUILTIN_ANNOTATION_TYPES, NEOSCRIPT_BUILTIN_NAMESPACES, NEOSCRIPT_BUILTIN_TYPES, NEO_VARIANT_TYPE_NAME, NEO_VARIANT_FOLDER_TYPE_NAME, NEO_LOOKUP_VARIANT_TYPE_NAME, NEO_LOOKUP_VARIANT_FOLDER_TYPE_NAME, NEO_VARIANT_SCOPE_NAME, NEO_VARIANT_RESERVED_NAME, NEO_VARIANT_FOLDER_PATH_SEPARATOR, NEOSCRIPT_OPERATORS, NEOSCRIPT_STATEMENT_SNIPPETS, STRING_TYPE, INT_TYPE, FLOAT_TYPE, BOOL_TYPE, IMAGE_REF_TYPE, SPRITE_INFO_TYPE, FLOAT_OPTIONAL_TYPE, NEOSCRIPT_STRUCTURED_LEAF_FIELDS, NEOSCRIPT_IMAGE_REGISTRY_TYPE, NEOSCRIPT_AUDIO_CLIP_REGISTRY_TYPE, NEOSCRIPT_PENDING_SYMBOL_ID_PREFIX, REGISTRY_ENTRY_TYPES, NEOSCRIPT_GLOBALS, NEOSCRIPT_SPRITE_INFO_EMPTY_MEMBER_ID, NEOSCRIPT_SPRITE_INFO_EMPTY_VALUE, NEOSCRIPT_MATH_TYPE_ID, MATH_INFERENCE_RULE, MATH_INTEGRAL_RULE, NEOSCRIPT_BUILTIN_TYPE_SYMBOLS;
1326
1340
  var init_language_spec = __esm({
1327
1341
  "../packages/neoscript-language/src/language-spec.ts"() {
1328
1342
  "use strict";
@@ -1384,7 +1398,7 @@ var init_language_spec = __esm({
1384
1398
  "bool",
1385
1399
  "string"
1386
1400
  ];
1387
- NEOSCRIPT_BUILTIN_TYPES = [
1401
+ NEOSCRIPT_BUILTIN_ANNOTATION_TYPES = [
1388
1402
  "Dictionary",
1389
1403
  "List",
1390
1404
  "Set",
@@ -1410,6 +1424,11 @@ var init_language_spec = __esm({
1410
1424
  "Vector3Int",
1411
1425
  "Color"
1412
1426
  ];
1427
+ NEOSCRIPT_BUILTIN_NAMESPACES = ["Math"];
1428
+ NEOSCRIPT_BUILTIN_TYPES = [
1429
+ ...NEOSCRIPT_BUILTIN_ANNOTATION_TYPES,
1430
+ ...NEOSCRIPT_BUILTIN_NAMESPACES
1431
+ ];
1413
1432
  NEO_VARIANT_TYPE_NAME = "NeoVariant";
1414
1433
  NEO_VARIANT_FOLDER_TYPE_NAME = "NeoVariantFolder";
1415
1434
  NEO_LOOKUP_VARIANT_TYPE_NAME = "NeoLookupVariant";
@@ -1658,6 +1677,9 @@ var init_language_spec = __esm({
1658
1677
  fileId: "",
1659
1678
  sliceIndex: 0
1660
1679
  });
1680
+ NEOSCRIPT_MATH_TYPE_ID = "builtin:Math";
1681
+ MATH_INFERENCE_RULE = "Accepts int, float, or decimal; the result mirrors the widest argument type.";
1682
+ MATH_INTEGRAL_RULE = "Accepts int, float, or decimal; int and float return int, decimal returns decimal.";
1661
1683
  NEOSCRIPT_BUILTIN_TYPE_SYMBOLS = [
1662
1684
  {
1663
1685
  id: "builtin:SpriteInfo",
@@ -1730,6 +1752,87 @@ var init_language_spec = __esm({
1730
1752
  name: "Color",
1731
1753
  kind: "builtin",
1732
1754
  members: structuredLeafFieldProperties("Color")
1755
+ },
1756
+ {
1757
+ id: NEOSCRIPT_MATH_TYPE_ID,
1758
+ name: "Math",
1759
+ kind: "builtin",
1760
+ documentation: "Numeric builtins with C# names and C# semantics. Math is a qualifier only \u2014 it names no type and holds no value, and every member is a pure function or constant.",
1761
+ members: [
1762
+ mathFunction(
1763
+ "Min",
1764
+ ["a", "b"],
1765
+ `Returns the smaller of two values. ${MATH_INFERENCE_RULE} Ties return the first argument.`
1766
+ ),
1767
+ mathFunction(
1768
+ "Max",
1769
+ ["a", "b"],
1770
+ `Returns the larger of two values. ${MATH_INFERENCE_RULE} Ties return the first argument.`
1771
+ ),
1772
+ mathFunction(
1773
+ "Clamp",
1774
+ ["value", "min", "max"],
1775
+ `Returns value pinned to the inclusive range min..max. ${MATH_INFERENCE_RULE} Requires min <= max.`
1776
+ ),
1777
+ mathFunction(
1778
+ "Round",
1779
+ ["x"],
1780
+ `Rounds to the nearest integer; midpoints round to even (banker's rounding). ${MATH_INTEGRAL_RULE}`,
1781
+ INT_TYPE
1782
+ ),
1783
+ mathFunction(
1784
+ "Floor",
1785
+ ["x"],
1786
+ `Rounds toward negative infinity. ${MATH_INTEGRAL_RULE}`,
1787
+ INT_TYPE
1788
+ ),
1789
+ mathFunction(
1790
+ "Ceiling",
1791
+ ["x"],
1792
+ `Rounds toward positive infinity. ${MATH_INTEGRAL_RULE}`,
1793
+ INT_TYPE
1794
+ ),
1795
+ mathFunction(
1796
+ "Truncate",
1797
+ ["x"],
1798
+ `Rounds toward zero. ${MATH_INTEGRAL_RULE}`,
1799
+ INT_TYPE
1800
+ ),
1801
+ mathFunction(
1802
+ "Abs",
1803
+ ["x"],
1804
+ `Returns the absolute value. ${MATH_INFERENCE_RULE}`
1805
+ ),
1806
+ mathFunction(
1807
+ "Sign",
1808
+ ["x"],
1809
+ "Returns -1, 0, or 1 for a negative, zero, or positive value. Accepts int, float, or decimal and always returns int.",
1810
+ INT_TYPE
1811
+ ),
1812
+ mathFunction(
1813
+ "Sqrt",
1814
+ ["x"],
1815
+ "Returns the square root. Accepts int or float and returns float; does not accept decimal."
1816
+ ),
1817
+ {
1818
+ ...property(
1819
+ "Math",
1820
+ "PI",
1821
+ FLOAT_TYPE,
1822
+ "The ratio of a circle's circumference to its diameter (3.141592653589793)."
1823
+ ),
1824
+ static: true
1825
+ },
1826
+ {
1827
+ ...property(
1828
+ "Math",
1829
+ "E",
1830
+ FLOAT_TYPE,
1831
+ "The base of the natural logarithm (2.718281828459045)."
1832
+ ),
1833
+ static: true
1834
+ }
1835
+ ]
1733
1836
  }
1734
1837
  ];
1735
1838
  }
@@ -2320,12 +2423,11 @@ function complete(snapshot, position) {
2320
2423
  word
2321
2424
  );
2322
2425
  } else {
2323
- const directStaticType = receiverTokens.length === 1 && receiverTokens[0]?.kind === "type" ? snapshot.project.typeByName.get(receiverTokens[0].text) : void 0;
2324
- const resolved = directStaticType ? {
2325
- type: { kind: "named", typeId: directStaticType.id },
2326
- staticType: directStaticType
2327
- } : resolveChain(snapshot, receiverTokens, offset);
2328
- candidates = completionItemsForResolution(snapshot, resolved, word);
2426
+ candidates = completionItemsForResolution(
2427
+ snapshot,
2428
+ resolveChain(snapshot, receiverTokens, offset),
2429
+ word
2430
+ );
2329
2431
  }
2330
2432
  } else if (tail?.kind === "identifier" && tail.text === NEOSCRIPT_CONSTRUCTOR_KEYWORD) {
2331
2433
  candidates = constructorCompletionItems(
@@ -3205,6 +3307,7 @@ function typeCompletionItems(snapshot, word) {
3205
3307
  )
3206
3308
  );
3207
3309
  for (const type of snapshot.project.typeByName.values()) {
3310
+ if (type.id === NEOSCRIPT_MATH_TYPE_ID) continue;
3208
3311
  items.push(typeCompletion(type, word, snapshot));
3209
3312
  }
3210
3313
  return items;
@@ -3409,9 +3512,16 @@ function switchCaseEnumContext(snapshot, offset) {
3409
3512
  function resolveChain(snapshot, tokens, offset) {
3410
3513
  if (tokens.length === 0) return null;
3411
3514
  const constructorType = tokens[0]?.kind === "identifier" && tokens[0].text === NEOSCRIPT_CONSTRUCTOR_KEYWORD && tokens[1]?.kind === "identifier" ? snapshot.project.typeByName.get(tokens[1].text) : void 0;
3515
+ const staticRoot = tokens[0]?.kind === "type" ? snapshot.project.typeByName.get(tokens[0].text) : void 0;
3412
3516
  let current;
3413
3517
  let cursor;
3414
- if (constructorType?.constructorSignature) {
3518
+ if (staticRoot) {
3519
+ current = {
3520
+ type: { kind: "named", typeId: staticRoot.id },
3521
+ staticType: staticRoot
3522
+ };
3523
+ cursor = 1;
3524
+ } else if (constructorType?.constructorSignature) {
3415
3525
  current = { type: constructorType.constructorSignature.returnType };
3416
3526
  const open = tokens.findIndex(
3417
3527
  (token, index) => index >= 2 && token.kind === "punctuation" && token.text === "("
@@ -4509,6 +4619,7 @@ var init_analyzer = __esm({
4509
4619
  RESERVED_NAMES = /* @__PURE__ */ new Set([
4510
4620
  ...NEOSCRIPT_KEYWORDS,
4511
4621
  ...NEOSCRIPT_PRIMITIVE_TYPES,
4622
+ ...NEOSCRIPT_BUILTIN_NAMESPACES,
4512
4623
  "Dictionary",
4513
4624
  "Set"
4514
4625
  ]);
@@ -8867,6 +8978,30 @@ function numericResult(left, right) {
8867
8978
  }
8868
8979
  return { kind: "primitive", name: "int" };
8869
8980
  }
8981
+ function isContextualNumericLiteral(expression) {
8982
+ if (expression.kind === "litInt" || expression.kind === "litFloat") {
8983
+ return true;
8984
+ }
8985
+ return expression.kind === "unary" && expression.op === "-" && (expression.operand.kind === "litInt" || expression.operand.kind === "litFloat");
8986
+ }
8987
+ function mathResultType(rule, joined, pos) {
8988
+ switch (rule) {
8989
+ case "join":
8990
+ return joined;
8991
+ case "integral":
8992
+ return isPrimitive(joined, "decimal") ? joined : { kind: "primitive", name: "int" };
8993
+ case "int":
8994
+ return { kind: "primitive", name: "int" };
8995
+ case "float":
8996
+ if (isPrimitive(joined, "decimal")) {
8997
+ throw new CompileError(
8998
+ "Math.Sqrt does not accept decimal; convert explicitly with value.ToFloat().",
8999
+ pos
9000
+ );
9001
+ }
9002
+ return { kind: "primitive", name: "float" };
9003
+ }
9004
+ }
8870
9005
  function normalizedReturnType(type, kind) {
8871
9006
  if (kind === "action" || kind === "setter" || kind === "constructor") {
8872
9007
  return { kind: "primitive", name: "null" };
@@ -9209,7 +9344,7 @@ function switchValueKey(value) {
9209
9344
  const enumId = value.typeInfo.type === 8 /* Enum */ ? value.typeInfo.enumId : null;
9210
9345
  return JSON.stringify([value.typeInfo.type, enumId, value.value]);
9211
9346
  }
9212
- var CONSTRUCTING_FUNCTION_KINDS, VARIANT_INITIALIZE_NAME, VARIANT_APPLY_NAME, WRITE_TARGET_DESCRIPTIONS, FALLTHROUGH_LABELS, Scope, StrictNeoScriptResolver, SPRITE_DERIVED_MEMBERS;
9347
+ var CONSTRUCTING_FUNCTION_KINDS, VARIANT_INITIALIZE_NAME, VARIANT_APPLY_NAME, MATH_NAMESPACE_NAME, MATH_FUNCTIONS, MATH_CONSTANTS, WRITE_TARGET_DESCRIPTIONS, FALLTHROUGH_LABELS, Scope, StrictNeoScriptResolver, SPRITE_DERIVED_MEMBERS;
9213
9348
  var init_strict_resolver = __esm({
9214
9349
  "../packages/neoscript-language/src/strict-resolver.ts"() {
9215
9350
  "use strict";
@@ -9233,6 +9368,23 @@ var init_strict_resolver = __esm({
9233
9368
  );
9234
9369
  VARIANT_INITIALIZE_NAME = "Initialize";
9235
9370
  VARIANT_APPLY_NAME = "ToVariant";
9371
+ MATH_NAMESPACE_NAME = "Math";
9372
+ MATH_FUNCTIONS = /* @__PURE__ */ new Map([
9373
+ ["Min", { op: "min", arity: 2, result: "join" }],
9374
+ ["Max", { op: "max", arity: 2, result: "join" }],
9375
+ ["Clamp", { op: "clamp", arity: 3, result: "join" }],
9376
+ ["Round", { op: "round", arity: 1, result: "integral" }],
9377
+ ["Floor", { op: "floor", arity: 1, result: "integral" }],
9378
+ ["Ceiling", { op: "ceiling", arity: 1, result: "integral" }],
9379
+ ["Truncate", { op: "truncate", arity: 1, result: "integral" }],
9380
+ ["Abs", { op: "abs", arity: 1, result: "join" }],
9381
+ ["Sign", { op: "sign", arity: 1, result: "int" }],
9382
+ ["Sqrt", { op: "sqrt", arity: 1, result: "float" }]
9383
+ ]);
9384
+ MATH_CONSTANTS = /* @__PURE__ */ new Map([
9385
+ ["PI", 3.141592653589793],
9386
+ ["E", 2.718281828459045]
9387
+ ]);
9236
9388
  WRITE_TARGET_DESCRIPTIONS = {
9237
9389
  this: "`this`",
9238
9390
  local: "a local",
@@ -10230,6 +10382,12 @@ var init_strict_resolver = __esm({
10230
10382
  };
10231
10383
  }
10232
10384
  assertLocalNameAvailable(name, scope, pos) {
10385
+ if (name === MATH_NAMESPACE_NAME) {
10386
+ throw new CompileError(
10387
+ `Cannot declare a local named '${MATH_NAMESPACE_NAME}' because it is a builtin namespace.`,
10388
+ pos
10389
+ );
10390
+ }
10233
10391
  if (!scope.lookup(name)) return;
10234
10392
  throw new CompileError(
10235
10393
  `Cannot declare local '${name}' because that name is already used in an enclosing or current scope.`,
@@ -11447,6 +11605,12 @@ var init_strict_resolver = __esm({
11447
11605
  false
11448
11606
  );
11449
11607
  }
11608
+ if (name === MATH_NAMESPACE_NAME) {
11609
+ throw new CompileError(
11610
+ `'${MATH_NAMESPACE_NAME}' is a builtin namespace, not a value.`,
11611
+ pos
11612
+ );
11613
+ }
11450
11614
  const type = this.project.typeByName.get(name);
11451
11615
  if (type?.kind === "enum") {
11452
11616
  return {
@@ -11844,6 +12008,15 @@ var init_strict_resolver = __esm({
11844
12008
  { ...NEOSCRIPT_SPRITE_INFO_EMPTY_VALUE }
11845
12009
  );
11846
12010
  }
12011
+ if (receiverAst.name === MATH_NAMESPACE_NAME) {
12012
+ if (optional) {
12013
+ throw new CompileError(
12014
+ `Optional chaining is not valid on builtin namespace '${MATH_NAMESPACE_NAME}'.`,
12015
+ pos
12016
+ );
12017
+ }
12018
+ return this.resolveMathMemberRead(name, pos);
12019
+ }
11847
12020
  const staticType = this.project.typeByName.get(receiverAst.name);
11848
12021
  if (staticType && staticType.kind !== "enum") {
11849
12022
  if (optional) {
@@ -12365,6 +12538,114 @@ var init_strict_resolver = __esm({
12365
12538
  );
12366
12539
  }
12367
12540
  }
12541
+ /**
12542
+ * P69 §2.7. A member read on the `Math` namespace: the two constants lower
12543
+ * to `float` literals, and nothing else on `Math` is readable as a value.
12544
+ */
12545
+ resolveMathMemberRead(name, pos) {
12546
+ const constant = MATH_CONSTANTS.get(name);
12547
+ if (constant !== void 0) {
12548
+ return literal({ kind: "primitive", name: "float" }, constant);
12549
+ }
12550
+ if (MATH_FUNCTIONS.has(name)) {
12551
+ throw new CompileError(
12552
+ `Math function '${MATH_NAMESPACE_NAME}.${name}' must be called with \`()\`; it cannot be read as a value.`,
12553
+ pos
12554
+ );
12555
+ }
12556
+ throw new CompileError(
12557
+ `${MATH_NAMESPACE_NAME} has no function '${name}'.`,
12558
+ pos
12559
+ );
12560
+ }
12561
+ /**
12562
+ * P69 §2. A call on the `Math` namespace. Every function is one symbol
12563
+ * whose result type is computed here from the resolved argument types —
12564
+ * the arithmetic operator join rule, verbatim — and lowers to a `mathOp`
12565
+ * intrinsic carrying the operands in declaration order.
12566
+ */
12567
+ resolveMathCall(name, args, scope, pos) {
12568
+ const fn = MATH_FUNCTIONS.get(name);
12569
+ if (!fn) {
12570
+ if (MATH_CONSTANTS.has(name)) {
12571
+ throw new CompileError(
12572
+ `${MATH_NAMESPACE_NAME}.${name} is a constant, not a function; read it without \`()\`.`,
12573
+ pos
12574
+ );
12575
+ }
12576
+ throw new CompileError(
12577
+ `${MATH_NAMESPACE_NAME} has no function '${name}'.`,
12578
+ pos
12579
+ );
12580
+ }
12581
+ const label = `${MATH_NAMESPACE_NAME}.${name}`;
12582
+ requireArgCount(label, args, fn.arity, pos);
12583
+ const resolved = args.map((argument2, index) => {
12584
+ const value = this.resolveExpression(argument2, scope);
12585
+ if (isNullable(value.type)) {
12586
+ throw new CompileError(
12587
+ `${label} requires non-optional arguments; got ${this.describe(value.type)} for argument ${index + 1}.`,
12588
+ argument2.pos
12589
+ );
12590
+ }
12591
+ if (!isNumeric(value.type)) {
12592
+ throw new CompileError(
12593
+ `${label} requires numeric arguments; got ${this.describe(value.type)} for argument ${index + 1}.`,
12594
+ argument2.pos
12595
+ );
12596
+ }
12597
+ return value;
12598
+ });
12599
+ const joined = this.joinMathArguments(args, resolved, scope, pos);
12600
+ return intrinsic(
12601
+ "mathOp" /* MathOp */,
12602
+ {
12603
+ op: fn.op,
12604
+ argPointers: joined.arguments.map((argument2) => argument2.pointer),
12605
+ ...isPrimitive(joined.type, "decimal") ? { decimal: true } : {}
12606
+ },
12607
+ mathResultType(fn.result, joined.type, pos)
12608
+ );
12609
+ }
12610
+ /**
12611
+ * P69 §2.1. The join of a `Math` call's argument types, after the second
12612
+ * resolution pass that gives numeric literals beside a `decimal` argument
12613
+ * their decimal reading.
12614
+ *
12615
+ * A call's argument list carries no declared expected type once typing is
12616
+ * inferred, so `Math.Max(price, 0.99)` would otherwise type `0.99` as
12617
+ * `float` and die on the mix rule with no decimal literal suffix to escape
12618
+ * with. Re-resolving from the preserved raw lexeme is the assignment-context
12619
+ * rule applied across the argument list, not a new coercion: a non-literal
12620
+ * `float` expression still refuses to mix.
12621
+ */
12622
+ joinMathArguments(args, resolved, scope, pos) {
12623
+ const decimalType = {
12624
+ kind: "primitive",
12625
+ name: "decimal"
12626
+ };
12627
+ const anyDecimal = resolved.some(
12628
+ (argument2) => isPrimitive(argument2.type, "decimal")
12629
+ );
12630
+ const settled = !anyDecimal ? resolved : resolved.map((argument2, index) => {
12631
+ const ast = requiredAt(args, index);
12632
+ if (!isContextualNumericLiteral(ast)) return argument2;
12633
+ return this.resolveExpression(ast, scope, decimalType);
12634
+ });
12635
+ if (anyDecimal && settled.some((argument2) => isPrimitive(argument2.type, "float"))) {
12636
+ throw new CompileError(
12637
+ "Decimal and Float cannot be mixed implicitly; convert explicitly with value.ToDecimal(digits) or value.ToFloat().",
12638
+ pos
12639
+ );
12640
+ }
12641
+ return {
12642
+ arguments: settled,
12643
+ type: settled.reduce(
12644
+ (left, right) => numericResult(left, right.type),
12645
+ { kind: "primitive", name: "int" }
12646
+ )
12647
+ };
12648
+ }
12368
12649
  resolveCall(callee, argumentsList2, scope, pos) {
12369
12650
  const variantCall = this.resolveVariantCall(
12370
12651
  callee,
@@ -12458,6 +12739,15 @@ var init_strict_resolver = __esm({
12458
12739
  );
12459
12740
  }
12460
12741
  if (callee.receiver.kind === "ident" && !scope.lookup(callee.receiver.name)) {
12742
+ if (callee.receiver.name === MATH_NAMESPACE_NAME) {
12743
+ if (callee.optional) {
12744
+ throw new CompileError(
12745
+ `Optional chaining is not valid on builtin namespace '${MATH_NAMESPACE_NAME}'.`,
12746
+ pos
12747
+ );
12748
+ }
12749
+ return this.resolveMathCall(callee.name, argumentsList2, scope, pos);
12750
+ }
12461
12751
  const staticType = this.project.typeByName.get(callee.receiver.name);
12462
12752
  if (staticType && staticType.kind !== "enum") {
12463
12753
  if (callee.optional) {
@@ -14318,6 +14608,12 @@ var init_strict_resolver = __esm({
14318
14608
  resolved = { kind: "primitive", name: type.name };
14319
14609
  break;
14320
14610
  case "named": {
14611
+ if (type.name === MATH_NAMESPACE_NAME) {
14612
+ throw new CompileError(
14613
+ `'${MATH_NAMESPACE_NAME}' is a builtin namespace, not a type.`,
14614
+ type.pos
14615
+ );
14616
+ }
14321
14617
  const builtin = namedPrimitive(type.name);
14322
14618
  if (builtin) {
14323
14619
  resolved = { kind: "primitive", name: builtin };
@@ -26195,7 +26491,7 @@ var init_project_source_type_checker = __esm({
26195
26491
  ]);
26196
26492
  BUILTIN_TYPES = /* @__PURE__ */ new Set([
26197
26493
  ...NEOSCRIPT_PRIMITIVE_TYPES,
26198
- ...NEOSCRIPT_BUILTIN_TYPES,
26494
+ ...NEOSCRIPT_BUILTIN_ANNOTATION_TYPES,
26199
26495
  "void",
26200
26496
  "null",
26201
26497
  "Project",
@@ -26213,7 +26509,10 @@ var init_project_source_type_checker = __esm({
26213
26509
  "NeoAudioClip",
26214
26510
  "ProjectRelation"
26215
26511
  ]);
26216
- RESERVED_NAMES2 = new Set(NEOSCRIPT_KEYWORDS);
26512
+ RESERVED_NAMES2 = /* @__PURE__ */ new Set([
26513
+ ...NEOSCRIPT_KEYWORDS,
26514
+ ...NEOSCRIPT_BUILTIN_NAMESPACES
26515
+ ]);
26217
26516
  }
26218
26517
  });
26219
26518
 
@@ -27116,7 +27415,7 @@ var init_project_source_analysis = __esm({
27116
27415
  init_project_source_parameter_defaults();
27117
27416
  BUILTIN_TYPE_NAMES = /* @__PURE__ */ new Set([
27118
27417
  ...NEOSCRIPT_PRIMITIVE_TYPES,
27119
- ...NEOSCRIPT_BUILTIN_TYPES,
27418
+ ...NEOSCRIPT_BUILTIN_ANNOTATION_TYPES,
27120
27419
  "void",
27121
27420
  "null",
27122
27421
  "Project",
@@ -27134,7 +27433,10 @@ var init_project_source_analysis = __esm({
27134
27433
  "NeoAudioClip",
27135
27434
  "ProjectRelation"
27136
27435
  ]);
27137
- RESERVED_NAMES3 = new Set(NEOSCRIPT_KEYWORDS);
27436
+ RESERVED_NAMES3 = /* @__PURE__ */ new Set([
27437
+ ...NEOSCRIPT_KEYWORDS,
27438
+ ...NEOSCRIPT_BUILTIN_NAMESPACES
27439
+ ]);
27138
27440
  SYSTEM_RECORD_ID_PREFIX = "system_";
27139
27441
  RFC_4122_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
27140
27442
  QUARANTINED_LEGACY_SYSTEM_ID_PREFIXES = [
@@ -27634,7 +27936,7 @@ function projectFallbackCompletions(analysis, document, position) {
27634
27936
  }
27635
27937
  for (const typeName of [
27636
27938
  ...NEOSCRIPT_PRIMITIVE_TYPES,
27637
- ...NEOSCRIPT_BUILTIN_TYPES
27939
+ ...NEOSCRIPT_BUILTIN_ANNOTATION_TYPES
27638
27940
  ]) {
27639
27941
  if (expression) continue;
27640
27942
  items.set(typeName, {
@@ -41565,6 +41867,18 @@ function isNSFunctionDecimalOp(value) {
41565
41867
  const digits = info.digitsPointer;
41566
41868
  return digits === void 0 || isNSPointer(digits);
41567
41869
  }
41870
+ function isNSFunctionMathOp(value) {
41871
+ const v = value;
41872
+ if (v?.type !== "mathOp" /* mathOp */) return false;
41873
+ const info = v.info;
41874
+ if (typeof info !== "object" || info === null) return false;
41875
+ if (!NS_MATH_OPS.includes(info.op)) return false;
41876
+ const args = info.argPointers;
41877
+ if (!Array.isArray(args)) return false;
41878
+ if (!args.every(isNSPointer)) return false;
41879
+ const decimal2 = info.decimal;
41880
+ return decimal2 === void 0 || decimal2 === true;
41881
+ }
41568
41882
  function isNSFunctionCount(value) {
41569
41883
  const v = value;
41570
41884
  if (v?.type !== "count" /* count */) return false;
@@ -41612,7 +41926,7 @@ function isNSFunction(value) {
41612
41926
  return isNSFunctionClassConstructor(value) || isNSFunctionClassClone(value) || isNSFunctionListIndex(value) || isNSFunctionSelect(value) || isNSFunctionFirst(value) || isNSFunctionFirstOrDefault(value) || isNSFunctionWhere(value) || isNSFunctionContains(value) || // stringOp was missing from this chain since its introduction — a
41613
41927
  // compiled getter using ToLower()/StartsWith() would fail pointer
41614
41928
  // validation anywhere isNSFunction gates. Fixed alongside decimalOp.
41615
- isNSFunctionStringOp(value) || isNSFunctionDecimalOp(value) || isNSFunctionCount(value) || isNSFunctionVisitCount(value) || isNSFunctionHasVisited(value) || isNSFunctionVectorConstructor(value) || isNSFunctionImageSlice(value) || isNSFunctionVariantInitialize(value) || isNSFunctionVariantApply(value) || isNSFunctionDeclaredConstructor(value);
41929
+ isNSFunctionStringOp(value) || isNSFunctionDecimalOp(value) || isNSFunctionMathOp(value) || isNSFunctionCount(value) || isNSFunctionVisitCount(value) || isNSFunctionHasVisited(value) || isNSFunctionVectorConstructor(value) || isNSFunctionImageSlice(value) || isNSFunctionVariantInitialize(value) || isNSFunctionVariantApply(value) || isNSFunctionDeclaredConstructor(value);
41616
41930
  }
41617
41931
  function isNSInstructionVariable(value) {
41618
41932
  const v = value;
@@ -41903,7 +42217,7 @@ function isNSVoidBody(value) {
41903
42217
  if (value.typeInfo.type !== 0 /* Null */) return false;
41904
42218
  return value.typeInfo.required === true;
41905
42219
  }
41906
- var NS_STRING_OPS, NS_DECIMAL_OPS;
42220
+ var NS_STRING_OPS, NS_DECIMAL_OPS, NS_MATH_OPS;
41907
42221
  var init_neoscript_guards = __esm({
41908
42222
  "../src/models/neoscript/neoscript-guards.ts"() {
41909
42223
  "use strict";
@@ -41924,6 +42238,18 @@ var init_neoscript_guards = __esm({
41924
42238
  "toFloat",
41925
42239
  "toDecimal"
41926
42240
  ];
42241
+ NS_MATH_OPS = [
42242
+ "min",
42243
+ "max",
42244
+ "clamp",
42245
+ "round",
42246
+ "floor",
42247
+ "ceiling",
42248
+ "truncate",
42249
+ "abs",
42250
+ "sign",
42251
+ "sqrt"
42252
+ ];
41927
42253
  }
41928
42254
  });
41929
42255
 
@@ -63087,6 +63413,25 @@ function compareDecimalStrings(a, b) {
63087
63413
  if (aligned.a > aligned.b) return 1;
63088
63414
  return 0;
63089
63415
  }
63416
+ function minDecimalStrings(a, b) {
63417
+ return compareDecimalStrings(a, b) <= 0 ? a : b;
63418
+ }
63419
+ function maxDecimalStrings(a, b) {
63420
+ return compareDecimalStrings(a, b) >= 0 ? a : b;
63421
+ }
63422
+ function clampDecimalStrings(value, min, max) {
63423
+ if (compareDecimalStrings(min, max) > 0) {
63424
+ throw new DecimalClampRangeError(MATH_CLAMP_RANGE_MESSAGE);
63425
+ }
63426
+ if (compareDecimalStrings(value, min) < 0) return min;
63427
+ if (compareDecimalStrings(value, max) > 0) return max;
63428
+ return value;
63429
+ }
63430
+ function absDecimalString(value) {
63431
+ const parts = parseDecimalArg(value, "value");
63432
+ const magnitude = parts.coefficient < 0n ? -parts.coefficient : parts.coefficient;
63433
+ return formatDecimalParts({ coefficient: magnitude, scale: parts.scale });
63434
+ }
63090
63435
  function addDecimalStrings(a, b) {
63091
63436
  const aligned = alignScales(parseDecimalArg(a, "a"), parseDecimalArg(b, "b"));
63092
63437
  return formatDecimalParts(
@@ -63149,6 +63494,32 @@ function roundDecimalString(value, digits) {
63149
63494
  const rounded = roundParts(parseDecimalArg(value, "value"), digits);
63150
63495
  return formatDecimalParts(assertWithinEnvelope(rounded, "round"));
63151
63496
  }
63497
+ function toIntegralParts(parts, direction) {
63498
+ if (parts.scale === 0) return parts;
63499
+ const divisor = pow10(parts.scale);
63500
+ const quotient = parts.coefficient / divisor;
63501
+ const remainder = parts.coefficient % divisor;
63502
+ if (remainder === 0n) return { coefficient: quotient, scale: 0 };
63503
+ if (direction === "floor" && remainder < 0n) {
63504
+ return { coefficient: quotient - 1n, scale: 0 };
63505
+ }
63506
+ if (direction === "ceiling" && remainder > 0n) {
63507
+ return { coefficient: quotient + 1n, scale: 0 };
63508
+ }
63509
+ return { coefficient: quotient, scale: 0 };
63510
+ }
63511
+ function floorDecimalString(value) {
63512
+ const parts = toIntegralParts(parseDecimalArg(value, "value"), "floor");
63513
+ return formatDecimalParts(assertWithinEnvelope(parts, "floor"));
63514
+ }
63515
+ function ceilingDecimalString(value) {
63516
+ const parts = toIntegralParts(parseDecimalArg(value, "value"), "ceiling");
63517
+ return formatDecimalParts(assertWithinEnvelope(parts, "ceiling"));
63518
+ }
63519
+ function truncateDecimalString(value) {
63520
+ const parts = toIntegralParts(parseDecimalArg(value, "value"), "truncate");
63521
+ return formatDecimalParts(assertWithinEnvelope(parts, "truncate"));
63522
+ }
63152
63523
  function divideDecimalStrings(a, b, digits) {
63153
63524
  assertDigitsInRange(digits, "divide");
63154
63525
  const dividend = parseDecimalArg(a, "a");
@@ -63229,7 +63600,7 @@ function floatFromDecimalString(value) {
63229
63600
  assertCanonicalInput(value, "value");
63230
63601
  return Number(value);
63231
63602
  }
63232
- var DecimalOverflowError, DecimalDivisionByZeroError, DecimalDigitsRangeError, DecimalNonFiniteError, TEN;
63603
+ var DecimalOverflowError, DecimalDivisionByZeroError, DecimalDigitsRangeError, DecimalNonFiniteError, DecimalClampRangeError, MATH_CLAMP_RANGE_MESSAGE, TEN;
63233
63604
  var init_decimal_math = __esm({
63234
63605
  "../src/models/decimal/decimal-math.ts"() {
63235
63606
  "use strict";
@@ -63242,6 +63613,9 @@ var init_decimal_math = __esm({
63242
63613
  };
63243
63614
  DecimalNonFiniteError = class extends Error {
63244
63615
  };
63616
+ DecimalClampRangeError = class extends Error {
63617
+ };
63618
+ MATH_CLAMP_RANGE_MESSAGE = "Math.Clamp requires min <= max.";
63245
63619
  TEN = 10n;
63246
63620
  }
63247
63621
  });
@@ -68130,7 +68504,7 @@ function coerceDecimalOperand(value, context) {
68130
68504
  );
68131
68505
  }
68132
68506
  function rethrowDecimalError(error) {
68133
- if (error instanceof DecimalOverflowError || error instanceof DecimalDivisionByZeroError || error instanceof DecimalDigitsRangeError || error instanceof DecimalNonFiniteError) {
68507
+ if (error instanceof DecimalOverflowError || error instanceof DecimalDivisionByZeroError || error instanceof DecimalDigitsRangeError || error instanceof DecimalNonFiniteError || error instanceof DecimalClampRangeError) {
68134
68508
  throw new NSGetterRuntimeError(error.message);
68135
68509
  }
68136
68510
  throw error;
@@ -68157,6 +68531,163 @@ function applyDecimalArithmetic(op, operands) {
68157
68531
  rethrowDecimalError(error);
68158
68532
  }
68159
68533
  }
68534
+ function mathArgPointer(info, index, fn) {
68535
+ const pointer = info.argPointers[index];
68536
+ if (pointer === void 0) {
68537
+ throw new NSGetterRuntimeError(
68538
+ `Math.${fn} is missing argument ${index + 1}.`
68539
+ );
68540
+ }
68541
+ return pointer;
68542
+ }
68543
+ function mathNumberArg(value, fn) {
68544
+ if (value === null || value === void 0) {
68545
+ throw new NSGetterRuntimeError(`Math.${fn} argument is null.`);
68546
+ }
68547
+ if (typeof value !== "number") {
68548
+ throw new NSGetterRuntimeError(
68549
+ `Math.${fn} argument is not numeric: ${typeof value}.`
68550
+ );
68551
+ }
68552
+ return value;
68553
+ }
68554
+ function mathDecimalArg(value, fn) {
68555
+ if (value === null || value === void 0) {
68556
+ throw new NSGetterRuntimeError(`Math.${fn} argument is null.`);
68557
+ }
68558
+ if (typeof value !== "string" && typeof value !== "number") {
68559
+ throw new NSGetterRuntimeError(
68560
+ `Math.${fn} argument is not numeric: ${typeof value}.`
68561
+ );
68562
+ }
68563
+ return coerceDecimalOperand(value, `Math.${fn}`);
68564
+ }
68565
+ function requireFiniteMathArg(value, fn) {
68566
+ if (!Number.isFinite(value)) {
68567
+ throw new NSGetterRuntimeError(`Math.${fn} requires a finite argument.`);
68568
+ }
68569
+ return value;
68570
+ }
68571
+ function roundHalfEven(value) {
68572
+ if (Math.abs(value) >= DOUBLE_INTEGRAL_MAGNITUDE) return value;
68573
+ const integral = Math.floor(value);
68574
+ const fraction = value - integral;
68575
+ if (fraction > 0.5) return integral + 1;
68576
+ if (fraction < 0.5) return integral;
68577
+ if (integral % 2 === 0) return integral;
68578
+ return integral + 1;
68579
+ }
68580
+ function evalMathOp(info, scope, ctx) {
68581
+ const fn = NS_MATH_OP_NAMES[info.op];
68582
+ if (info.decimal === true) return evalDecimalMathOp(info, fn, scope, ctx);
68583
+ const value = mathNumberArg(
68584
+ evalPointer(mathArgPointer(info, 0, fn), scope, ctx),
68585
+ fn
68586
+ );
68587
+ switch (info.op) {
68588
+ case "min":
68589
+ return Math.min(
68590
+ value,
68591
+ mathNumberArg(evalPointer(mathArgPointer(info, 1, fn), scope, ctx), fn)
68592
+ );
68593
+ case "max":
68594
+ return Math.max(
68595
+ value,
68596
+ mathNumberArg(evalPointer(mathArgPointer(info, 1, fn), scope, ctx), fn)
68597
+ );
68598
+ case "clamp": {
68599
+ const min = mathNumberArg(
68600
+ evalPointer(mathArgPointer(info, 1, fn), scope, ctx),
68601
+ fn
68602
+ );
68603
+ const max = mathNumberArg(
68604
+ evalPointer(mathArgPointer(info, 2, fn), scope, ctx),
68605
+ fn
68606
+ );
68607
+ if (min > max) {
68608
+ throw new NSGetterRuntimeError(MATH_CLAMP_RANGE_MESSAGE);
68609
+ }
68610
+ if (value < min) return min;
68611
+ if (value > max) return max;
68612
+ return value;
68613
+ }
68614
+ case "round":
68615
+ return roundHalfEven(requireFiniteMathArg(value, fn));
68616
+ case "floor":
68617
+ return Math.floor(requireFiniteMathArg(value, fn));
68618
+ case "ceiling":
68619
+ return Math.ceil(requireFiniteMathArg(value, fn));
68620
+ case "truncate":
68621
+ return Math.trunc(requireFiniteMathArg(value, fn));
68622
+ case "abs":
68623
+ return Math.abs(value);
68624
+ case "sign":
68625
+ if (Number.isNaN(value)) {
68626
+ throw new NSGetterRuntimeError("Math.Sign is undefined for NaN.");
68627
+ }
68628
+ if (value > 0) return 1;
68629
+ if (value < 0) return -1;
68630
+ return 0;
68631
+ case "sqrt":
68632
+ return Math.sqrt(value);
68633
+ }
68634
+ }
68635
+ function evalDecimalMathOp(info, fn, scope, ctx) {
68636
+ const value = mathDecimalArg(
68637
+ evalPointer(mathArgPointer(info, 0, fn), scope, ctx),
68638
+ fn
68639
+ );
68640
+ try {
68641
+ switch (info.op) {
68642
+ case "min":
68643
+ return minDecimalStrings(
68644
+ value,
68645
+ mathDecimalArg(
68646
+ evalPointer(mathArgPointer(info, 1, fn), scope, ctx),
68647
+ fn
68648
+ )
68649
+ );
68650
+ case "max":
68651
+ return maxDecimalStrings(
68652
+ value,
68653
+ mathDecimalArg(
68654
+ evalPointer(mathArgPointer(info, 1, fn), scope, ctx),
68655
+ fn
68656
+ )
68657
+ );
68658
+ case "clamp":
68659
+ return clampDecimalStrings(
68660
+ value,
68661
+ mathDecimalArg(
68662
+ evalPointer(mathArgPointer(info, 1, fn), scope, ctx),
68663
+ fn
68664
+ ),
68665
+ mathDecimalArg(
68666
+ evalPointer(mathArgPointer(info, 2, fn), scope, ctx),
68667
+ fn
68668
+ )
68669
+ );
68670
+ case "round":
68671
+ return roundDecimalString(value, 0);
68672
+ case "floor":
68673
+ return floorDecimalString(value);
68674
+ case "ceiling":
68675
+ return ceilingDecimalString(value);
68676
+ case "truncate":
68677
+ return truncateDecimalString(value);
68678
+ case "abs":
68679
+ return absDecimalString(value);
68680
+ case "sign":
68681
+ return compareDecimalStrings(value, "0");
68682
+ case "sqrt":
68683
+ throw new NSGetterRuntimeError(
68684
+ "Math.Sqrt does not accept decimal arguments."
68685
+ );
68686
+ }
68687
+ } catch (error) {
68688
+ rethrowDecimalError(error);
68689
+ }
68690
+ }
68160
68691
  function stringifyForInterp(v) {
68161
68692
  if (v === null || v === void 0) return "";
68162
68693
  if (typeof v === "string") return v;
@@ -68676,6 +69207,8 @@ function evalFunction(fn, scope, ctx) {
68676
69207
  }
68677
69208
  break;
68678
69209
  }
69210
+ case "mathOp" /* mathOp */:
69211
+ return evalMathOp(fn.info, scope, ctx);
68679
69212
  case "stringOp" /* stringOp */: {
68680
69213
  const receiver = evalPointer(fn.info.receiverPointer, scope, ctx);
68681
69214
  if (typeof receiver !== "string") {
@@ -71888,7 +72421,7 @@ function iterateCollection(c, ctx, callback) {
71888
72421
  return callback(entry, key, valueId);
71889
72422
  });
71890
72423
  }
71891
- var DELEGATE_LEXICAL_THIS, DELEGATE_LEXICAL_ROOT, NonCatchableNSGetterRuntimeError, NativeFunctionDelegateUnavailableError, CorruptNeoScriptIRError, NeoScriptResourceLimitError, NeoScriptWallClockTimeoutError, DEFAULT_NEO_SCRIPT_EXECUTION_BUDGET_LIMITS, liveListIndexesByProject, evaluatorOwnershipCachesByBase, MAX_CONSTRUCTION_DEPTH, MAX_LOOP_ITERATIONS, resolutionCacheByMembers, NO_SCHEMA_REVISION, LazyValueOverlay, READONLY_FOREACH_BINDING_ERROR, READONLY_CATCH_BINDING_ERROR, EMPTY_SUPPLIED_CONSTRUCTOR_FIELDS;
72424
+ var DELEGATE_LEXICAL_THIS, DELEGATE_LEXICAL_ROOT, NonCatchableNSGetterRuntimeError, NativeFunctionDelegateUnavailableError, CorruptNeoScriptIRError, NeoScriptResourceLimitError, NeoScriptWallClockTimeoutError, DEFAULT_NEO_SCRIPT_EXECUTION_BUDGET_LIMITS, liveListIndexesByProject, evaluatorOwnershipCachesByBase, MAX_CONSTRUCTION_DEPTH, MAX_LOOP_ITERATIONS, resolutionCacheByMembers, NO_SCHEMA_REVISION, LazyValueOverlay, READONLY_FOREACH_BINDING_ERROR, READONLY_CATCH_BINDING_ERROR, NS_MATH_OP_NAMES, DOUBLE_INTEGRAL_MAGNITUDE, EMPTY_SUPPLIED_CONSTRUCTOR_FIELDS;
71892
72425
  var init_evaluateNSGetter = __esm({
71893
72426
  "../src/view-models/neoscript-evaluator/evaluateNSGetter.ts"() {
71894
72427
  "use strict";
@@ -72032,6 +72565,19 @@ var init_evaluateNSGetter = __esm({
72032
72565
  };
72033
72566
  READONLY_FOREACH_BINDING_ERROR = "Cannot assign to a read-only foreach iterator binding.";
72034
72567
  READONLY_CATCH_BINDING_ERROR = "Cannot assign to a read-only catch message binding.";
72568
+ NS_MATH_OP_NAMES = {
72569
+ min: "Min",
72570
+ max: "Max",
72571
+ clamp: "Clamp",
72572
+ round: "Round",
72573
+ floor: "Floor",
72574
+ ceiling: "Ceiling",
72575
+ truncate: "Truncate",
72576
+ abs: "Abs",
72577
+ sign: "Sign",
72578
+ sqrt: "Sqrt"
72579
+ };
72580
+ DOUBLE_INTEGRAL_MAGNITUDE = 2 ** 53;
72035
72581
  EMPTY_SUPPLIED_CONSTRUCTOR_FIELDS = [];
72036
72582
  }
72037
72583
  });
@@ -111706,7 +112252,7 @@ var init_registry2 = __esm({
111706
112252
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
111707
112253
  formatVersion: 3,
111708
112254
  contractVersion: "3.13",
111709
- cliVersion: "0.31.17",
112255
+ cliVersion: "0.32.0",
111710
112256
  projectFileUploadBatchSize: 32,
111711
112257
  documentRecords: {
111712
112258
  member: {
@@ -118301,7 +118847,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
118301
118847
  async function main() {
118302
118848
  const args = parseArgs(process.argv.slice(2));
118303
118849
  if (args.command === "--version") {
118304
- console.log("0.31.17");
118850
+ console.log("0.32.0");
118305
118851
  return;
118306
118852
  }
118307
118853
  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.31.17",
3
+ "version": "0.32.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.31.17 -->
12
+ <!-- reviewed-through-cli: 0.32.0 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -138,7 +138,8 @@ completely before editing:
138
138
  variant folders, partial animation overrides, segments, tracks, and NeoFlow
139
139
  dialogues.
140
140
  - [NeoScript](references/neoscript.md): inline bodies, snippets, nullability,
141
- loops, switch, try/catch, ownership, evaluation, and migrations.
141
+ numeric builtins, loops, switch, try/catch, ownership, evaluation, and
142
+ migrations.
142
143
  - [Commands and synchronization](references/commands-and-sync.md): pull/push,
143
144
  conflicts, low-level repair, branches, releases, and history.
144
145
  - [CLI development](references/cli-development.md): implementation source of
@@ -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.31.17 -->
86
+ <!-- reviewed-through-cli: 0.32.0 -->
87
87
  ```
88
88
 
89
89
  The quoted version above is checked too, so this instruction cannot go stale
@@ -7,6 +7,7 @@ runtime ownership, evaluation, and migrations.
7
7
 
8
8
  - Inline bodies and snippets
9
9
  - Nullability and ownership
10
+ - Numeric builtins
10
11
  - Logical operators
11
12
  - Loops and transfer
12
13
  - Switch
@@ -81,6 +82,27 @@ Save, Session, Setter, or otherwise writable. A constructor writes only its
81
82
  `this` instance. A writable structured-leaf field still inherits the receiver's
82
83
  effective storage restrictions.
83
84
 
85
+ ## Numeric builtins
86
+
87
+ Use the `Math` namespace for numeric utilities with C# names and C# semantics:
88
+ `Min`, `Max`, `Clamp`, `Round`, `Floor`, `Ceiling`, `Truncate`, `Abs`, `Sign`,
89
+ `Sqrt`, and the `PI` and `E` constants.
90
+
91
+ ```neo
92
+ return Math.Clamp(this.Quality + bonus, 0, 10);
93
+ ```
94
+
95
+ Each result follows the arithmetic join of its argument types: all-`int` gives
96
+ `int`, any `float` gives `float`, any `decimal` gives `decimal`, and `decimal`
97
+ never mixes with `float`. `Round`, `Floor`, `Ceiling`, and `Truncate` are the
98
+ language's `float`-to-`int` conversions: they return `int` for `int` and
99
+ `float` input, keep a `decimal` a `decimal`, and reject a non-finite argument
100
+ at runtime. `Round` rounds midpoints to even, matching `decimal.Round(0)`.
101
+ `Sign` always returns `int`; `Sqrt` takes `int` or `float` only.
102
+
103
+ `Math` is a qualifier, never a type or a value, and the name is reserved: no
104
+ Class, enum, member, parameter, or local may be named `Math`.
105
+
84
106
  ## Logical operators
85
107
 
86
108
  Use `&&` and `||` with C# precedence: `&&` binds tighter than `||`, and