flocc 0.5.22 → 0.5.23

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/dist/flocc.es.js CHANGED
@@ -817,7 +817,37 @@ var Operators;
817
817
  Operators["agent"] = "agent";
818
818
  Operators["environment"] = "environment";
819
819
  Operators["vector"] = "vector";
820
+ Operators["log"] = "log";
820
821
  })(Operators || (Operators = {}));
822
+ var operatorInfo = {
823
+ add: { min: 2, max: 2 },
824
+ subtract: { min: 2, max: 2 },
825
+ multiply: { min: 2, max: 2 },
826
+ divide: { min: 2, max: 2 },
827
+ mod: { min: 2, max: 2 },
828
+ power: { min: 2, max: 2 },
829
+ get: { min: 1, max: 1 },
830
+ set: { min: 2, max: 2 },
831
+ enqueue: { min: 2, max: 2 },
832
+ local: { min: 1, max: 2 },
833
+ if: { min: 2, max: 3 },
834
+ and: { min: 2, max: 2 },
835
+ or: { min: 2, max: 2 },
836
+ gt: { min: 2, max: 2 },
837
+ gte: { min: 2, max: 2 },
838
+ lt: { min: 2, max: 2 },
839
+ lte: { min: 2, max: 2 },
840
+ eq: { min: 2, max: 2 },
841
+ map: { min: 2, max: 2 },
842
+ filter: { min: 2, max: 2 },
843
+ key: { min: 2, max: 2 },
844
+ method: { min: 2, max: Infinity },
845
+ agent: { min: 0, max: 0 },
846
+ environment: { min: 0, max: 0 },
847
+ vector: { min: 1, max: Infinity },
848
+ log: { min: 1, max: Infinity }
849
+ };
850
+ var ARITHMETIC_OPS = new Set(["add", "subtract", "multiply", "divide", "mod", "power"]);
821
851
  var add = function (a, b) { return a + b; };
822
852
  var subtract = function (a, b) { return a - b; };
823
853
  var multiply = function (a, b) { return a * b; };
@@ -841,6 +871,127 @@ var method = function (obj, name) {
841
871
  return null;
842
872
  return obj[name].apply(obj, args);
843
873
  };
874
+ /**
875
+ * Format a single value for step display. If isFirst is true and the value is
876
+ * a known operator string, render it bare (unquoted).
877
+ */
878
+ function formatStepValue(val, isFirst) {
879
+ if (val === null || val === undefined)
880
+ return String(val);
881
+ if (typeof val === "number" || typeof val === "boolean")
882
+ return String(val);
883
+ if (typeof val === "string") {
884
+ if (isFirst && val in Operators)
885
+ return val;
886
+ return JSON.stringify(val);
887
+ }
888
+ if (val instanceof Array) {
889
+ if (val.length === 0)
890
+ return "[]";
891
+ var parts = val.map(function (s, i) { return formatStepValue(s, i === 0); });
892
+ return "(" + parts.join(" ") + ")";
893
+ }
894
+ if (typeof val === "object") {
895
+ try {
896
+ return JSON.stringify(val);
897
+ }
898
+ catch (_a) {
899
+ return String(val);
900
+ }
901
+ }
902
+ return String(val);
903
+ }
904
+ /**
905
+ * Format a step into a readable string representation for logging.
906
+ */
907
+ function formatStep(step) {
908
+ return formatStepValue(step, false);
909
+ }
910
+ /**
911
+ * Format a value as an S-expression atom (no recursion into arrays).
912
+ */
913
+ function formatAtom(val) {
914
+ if (val === null || val === undefined)
915
+ return "null";
916
+ if (typeof val === "number" || typeof val === "boolean")
917
+ return String(val);
918
+ if (typeof val === "string")
919
+ return JSON.stringify(val);
920
+ if (typeof val === "object" && !Array.isArray(val)) {
921
+ try {
922
+ return JSON.stringify(val);
923
+ }
924
+ catch (_a) {
925
+ return "[object]";
926
+ }
927
+ }
928
+ return String(val);
929
+ }
930
+ /**
931
+ * Format a value for S-expression display. The first element (operator) of
932
+ * a step is rendered as a bare identifier; other string values are quoted.
933
+ */
934
+ function formatSexpValue(val, isOperator) {
935
+ if (isOperator && typeof val === "string")
936
+ return val;
937
+ return formatAtom(val);
938
+ }
939
+ /**
940
+ * Pretty-print a single step as an S-expression with width-based wrapping.
941
+ */
942
+ function prettyFormatStep(step, indentUnit, maxLineWidth, currentIndent) {
943
+ if (!Array.isArray(step))
944
+ return formatAtom(step);
945
+ if (step.length === 0)
946
+ return "()";
947
+ // Try compact first
948
+ var compact = "(" + step.map(function (s, i) {
949
+ if (Array.isArray(s))
950
+ return prettyFormatStep(s, indentUnit, Infinity, "");
951
+ return formatSexpValue(s, i === 0);
952
+ }).join(" ") + ")";
953
+ if (currentIndent.length + compact.length <= maxLineWidth)
954
+ return compact;
955
+ // Wrap: operator on first line, each arg indented
956
+ var op = Array.isArray(step[0]) ? prettyFormatStep(step[0], indentUnit, maxLineWidth, currentIndent) : formatSexpValue(step[0], true);
957
+ var args = step.slice(1);
958
+ if (args.length === 0)
959
+ return "(" + op + ")";
960
+ var childIndent = currentIndent + indentUnit;
961
+ var formattedArgs = args.map(function (a) { return prettyFormatStep(a, indentUnit, maxLineWidth, childIndent); });
962
+ return "(" + op + "\n" + formattedArgs.map(function (a) { return childIndent + a; }).join("\n") + ")";
963
+ }
964
+ function validOperatorNames() {
965
+ return Object.keys(operatorInfo).join(", ");
966
+ }
967
+ function checkArity(op, argCount, strict) {
968
+ if (strict === void 0) { strict = false; }
969
+ var info = operatorInfo[op];
970
+ if (!info)
971
+ return null;
972
+ if (info.min === info.max) {
973
+ if (argCount < info.min) {
974
+ return "Rule: \"" + op + "\" expects " + info.min + " argument" + (info.min !== 1 ? "s" : "") + ", got " + argCount;
975
+ }
976
+ if (strict && argCount > info.max) {
977
+ return "Rule: \"" + op + "\" expects " + info.min + " argument" + (info.min !== 1 ? "s" : "") + ", got " + argCount;
978
+ }
979
+ }
980
+ else if (info.max === Infinity) {
981
+ if (argCount < info.min) {
982
+ return "Rule: \"" + op + "\" expects at least " + info.min + " argument" + (info.min !== 1 ? "s" : "") + ", got " + argCount;
983
+ }
984
+ }
985
+ else {
986
+ if (argCount < info.min) {
987
+ return "Rule: \"" + op + "\" expects " + info.min + "-" + info.max + " arguments, got " + argCount;
988
+ }
989
+ if (strict && argCount > info.max) {
990
+ return "Rule: \"" + op + "\" expects " + info.min + "-" + info.max + " arguments, got " + argCount;
991
+ }
992
+ }
993
+ return null;
994
+ }
844
995
  /**
845
996
  * The `Rule` class is an experimental interface for adding behavior to {@linkcode Agent}s. A `Rule` object may be used in place of a `tick` function to be added as `Agent` behavior using `agent.set('tick', tickRule)`. As a trivial example, consider the following `Rule`, which increments the `Agent`'s `x` value with every time step:
846
997
  *
@@ -895,6 +1046,7 @@ var Rule = /** @class */ (function () {
895
1046
  * |`"agent"`|`0`|No arguments; returns the `Agent`|
896
1047
  * |`"environment"`|`0`|No arguments, returns the `Environment`|
897
1048
  * |`"vector"`|`any`|Creates an n-dimensional {@linkcode Vector} from the supplied arguments|
1049
+ * |`"log"`|`any`|Logs expression(s) and returns the last evaluated value|
898
1050
  */
899
1051
  function Rule(environment, steps) {
900
1052
  var _this = this;
@@ -903,10 +1055,12 @@ var Rule = /** @class */ (function () {
903
1055
  /** @hidden */
904
1056
  this.locals = {};
905
1057
  /**
906
- * interpret single array step
907
- * @since 0.3.0
908
- * @hidden
1058
+ * When true, evaluation traces are logged and captured.
909
1059
  */
1060
+ this.trace = false;
1061
+ this.traceLog = [];
1062
+ /** @hidden */
1063
+ this._traceDepth = 0;
910
1064
  this.evaluate = function (agent, step) {
911
1065
  var first = step && step.length > 0 ? step[0] : null;
912
1066
  if (first === undefined || first === null)
@@ -917,30 +1071,79 @@ var Rule = /** @class */ (function () {
917
1071
  return _this.evaluate(agent, step.slice(1));
918
1072
  return innerStep;
919
1073
  }
920
- if (first === "log") {
921
- console.log("logging", step.slice(1), _this.evaluate(agent, step.slice(1)));
922
- return null;
1074
+ // Trace: check if this is a known operator call to trace
1075
+ var shouldTrace = _this.trace && typeof first === "string" && first in Operators;
1076
+ if (shouldTrace)
1077
+ _this._traceDepth++;
1078
+ var result = _this._evaluateOp(agent, step, first);
1079
+ if (shouldTrace) {
1080
+ _this._traceLogEntry(step, result);
1081
+ _this._traceDepth--;
1082
+ }
1083
+ return result;
1084
+ };
1085
+ /** @hidden */
1086
+ this._evaluateOp = function (agent, step, first) {
1087
+ if (first === Operators.log) {
1088
+ var args = step.slice(1);
1089
+ if (args.length === 0) {
1090
+ console.warn("Rule: \"log\" expects at least 1 argument, got 0");
1091
+ return null;
1092
+ }
1093
+ var lastValue = null;
1094
+ for (var i = 0; i < args.length; i++) {
1095
+ var expr = args[i];
1096
+ var evaluated = _this.evaluate(agent, [expr]);
1097
+ console.log("Rule log: " + formatStep(expr) + " \u2192 " + formatStep(evaluated));
1098
+ lastValue = evaluated;
1099
+ }
1100
+ return lastValue;
923
1101
  }
924
1102
  if (!(first in Operators) && step.length > 1) {
1103
+ if (typeof first === "string") {
1104
+ console.warn("Rule: unknown operator \"" + first + "\". Valid operators: " + validOperatorNames());
1105
+ }
925
1106
  return step;
926
1107
  }
1108
+ var argCount = step.length - 1;
1109
+ // Arity check at runtime for known operators
1110
+ if (typeof first === "string" && first in Operators) {
1111
+ var arityMsg = checkArity(first, argCount);
1112
+ if (arityMsg) {
1113
+ console.warn(arityMsg);
1114
+ }
1115
+ }
927
1116
  var a = step.length > 1 ? [step[1]] : undefined;
928
1117
  var b = step.length > 2 ? [step[2]] : undefined;
929
1118
  var c = step.length > 3 ? [step[3]] : undefined;
930
- if (first === Operators.add)
931
- return add(_this.evaluate(agent, a), _this.evaluate(agent, b));
932
- if (first === Operators.subtract)
933
- return subtract(_this.evaluate(agent, a), _this.evaluate(agent, b));
934
- if (first === Operators.multiply)
935
- return multiply(_this.evaluate(agent, a), _this.evaluate(agent, b));
936
- if (first === Operators.divide)
937
- return divide(_this.evaluate(agent, a), _this.evaluate(agent, b));
938
- if (first === Operators.mod)
939
- return mod(_this.evaluate(agent, a), _this.evaluate(agent, b));
940
- if (first === Operators.power)
941
- return power(_this.evaluate(agent, a), _this.evaluate(agent, b));
942
- if (first === Operators.get)
943
- return get(agent, _this.evaluate(agent, a));
1119
+ // Arithmetic operators with type checking
1120
+ if (ARITHMETIC_OPS.has(first)) {
1121
+ var va = _this.evaluate(agent, a);
1122
+ var vb = _this.evaluate(agent, b);
1123
+ if (typeof va !== "number" || typeof vb !== "number") {
1124
+ console.warn("Rule: \"" + first + "\" expects numeric arguments, got " + typeof va + " and " + typeof vb);
1125
+ }
1126
+ if (first === Operators.add)
1127
+ return add(va, vb);
1128
+ if (first === Operators.subtract)
1129
+ return subtract(va, vb);
1130
+ if (first === Operators.multiply)
1131
+ return multiply(va, vb);
1132
+ if (first === Operators.divide)
1133
+ return divide(va, vb);
1134
+ if (first === Operators.mod)
1135
+ return mod(va, vb);
1136
+ if (first === Operators.power)
1137
+ return power(va, vb);
1138
+ }
1139
+ if (first === Operators.get) {
1140
+ var keyName = _this.evaluate(agent, a);
1141
+ var result = get(agent, keyName);
1142
+ if (result === null || result === undefined) {
1143
+ console.warn("Rule: \"get\" returned null for key \"" + keyName + "\" \u2014 key may not exist on agent");
1144
+ }
1145
+ return result;
1146
+ }
944
1147
  if (first === Operators.set)
945
1148
  return set(agent, _this.evaluate(agent, a), _this.evaluate(agent, b));
946
1149
  if (first === Operators.enqueue) {
@@ -1028,13 +1231,151 @@ var Rule = /** @class */ (function () {
1028
1231
  this.environment = environment;
1029
1232
  this.steps = steps;
1030
1233
  }
1234
+ /**
1235
+ * Format arbitrary steps as pretty-printed S-expressions.
1236
+ */
1237
+ Rule.formatSteps = function (steps, options) {
1238
+ var indent = (options && options.indent) || " ";
1239
+ var maxLineWidth = (options && options.maxLineWidth) || 60;
1240
+ // Check if it's multi-step (array of arrays)
1241
+ var isMultiStep = steps.length > 0 && Array.isArray(steps[0]);
1242
+ if (isMultiStep) {
1243
+ return steps.map(function (s) { return prettyFormatStep(s, indent, maxLineWidth, ""); }).join("\n\n");
1244
+ }
1245
+ return prettyFormatStep(steps, indent, maxLineWidth, "");
1246
+ };
1247
+ /**
1248
+ * Pretty-print the rule's step tree as S-expressions.
1249
+ */
1250
+ Rule.prototype.format = function (options) {
1251
+ return Rule.formatSteps(this.steps, options);
1252
+ };
1253
+ /**
1254
+ * Returns a formatted S-expression representation of the rule.
1255
+ */
1256
+ Rule.prototype.toString = function () {
1257
+ return this.format();
1258
+ };
1259
+ /**
1260
+ * Validate the rule's step tree and return an array of diagnostics.
1261
+ * Does not throw — returns diagnostics for inspection.
1262
+ * @since 0.6.0
1263
+ */
1264
+ Rule.prototype.validate = function () {
1265
+ var diagnostics = [];
1266
+ if (!this.steps || (Array.isArray(this.steps) && this.steps.length === 0)) {
1267
+ diagnostics.push({
1268
+ path: "root",
1269
+ level: "warning",
1270
+ message: "Empty steps array"
1271
+ });
1272
+ return diagnostics;
1273
+ }
1274
+ // Check if steps is a single step (first element is a string operator)
1275
+ // or an array of steps (first element is an array)
1276
+ var isMultiStep = Array.isArray(this.steps[0]);
1277
+ if (isMultiStep) {
1278
+ // Multiple top-level steps
1279
+ for (var i = 0; i < this.steps.length; i++) {
1280
+ var step = this.steps[i];
1281
+ if (!Array.isArray(step)) {
1282
+ diagnostics.push({
1283
+ path: "[" + i + "]",
1284
+ level: "warning",
1285
+ message: "Step is a bare value (" + typeof step + ": " + JSON.stringify(step) + ") instead of an array"
1286
+ });
1287
+ }
1288
+ else {
1289
+ this._validateStep(step, "[" + i + "]", diagnostics);
1290
+ }
1291
+ }
1292
+ }
1293
+ else {
1294
+ // Single step (the steps array IS the step)
1295
+ this._validateStep(this.steps, "root", diagnostics);
1296
+ }
1297
+ return diagnostics;
1298
+ };
1299
+ /** @hidden */
1300
+ Rule.prototype._validateStep = function (step, path, diagnostics) {
1301
+ if (step.length === 0) {
1302
+ diagnostics.push({
1303
+ path: path,
1304
+ level: "warning",
1305
+ message: "Empty step array"
1306
+ });
1307
+ return;
1308
+ }
1309
+ var first = step[0];
1310
+ // If first element is an array, it's a nested step sequence
1311
+ if (Array.isArray(first)) {
1312
+ this._validateStep(first, path + "[0]", diagnostics);
1313
+ for (var i = 1; i < step.length; i++) {
1314
+ if (Array.isArray(step[i])) {
1315
+ this._validateStep(step[i], path + ("[" + i + "]"), diagnostics);
1316
+ }
1317
+ }
1318
+ return;
1319
+ }
1320
+ if (typeof first === "string") {
1321
+ var argCount = step.length - 1;
1322
+ // Check if operator is known
1323
+ if (!(first in Operators)) {
1324
+ if (step.length > 1) {
1325
+ diagnostics.push({
1326
+ path: path,
1327
+ level: "error",
1328
+ message: "Unknown operator \"" + first + "\". Valid operators: " + validOperatorNames()
1329
+ });
1330
+ }
1331
+ return;
1332
+ }
1333
+ // Check arity (strict: also warn on too many args)
1334
+ var arityMsg = checkArity(first, argCount, true);
1335
+ if (arityMsg) {
1336
+ diagnostics.push({
1337
+ path: path,
1338
+ level: "error",
1339
+ message: arityMsg
1340
+ });
1341
+ }
1342
+ // Recurse into sub-steps
1343
+ for (var i = 1; i < step.length; i++) {
1344
+ if (Array.isArray(step[i])) {
1345
+ this._validateStep(step[i], path + ("[" + i + "]"), diagnostics);
1346
+ }
1347
+ }
1348
+ }
1349
+ };
1350
+ /**
1351
+ * interpret single array step
1352
+ * @since 0.3.0
1353
+ * @hidden
1354
+ */
1355
+ /** @hidden */
1356
+ Rule.prototype._traceLogEntry = function (step, result) {
1357
+ var indent = " ".repeat(this._traceDepth - 1);
1358
+ var formatted = formatStep(step);
1359
+ var resultFormatted = formatStep(result);
1360
+ var line = "Rule trace: " + indent + formatted + " \u2192 " + resultFormatted;
1361
+ console.log(line);
1362
+ this.traceLog.push(line);
1363
+ };
1031
1364
  /**
1032
1365
  * @since 0.3.0
1033
1366
  * @hidden
1034
1367
  */
1035
1368
  Rule.prototype.call = function (agent) {
1369
+ if (this.trace) {
1370
+ this.traceLog = [];
1371
+ this._traceDepth = 0;
1372
+ }
1036
1373
  return this.evaluate(agent, this.steps);
1037
1374
  };
1375
+ /**
1376
+ * Static operator info mapping operator names to their expected argument counts.
1377
+ */
1378
+ Rule.operatorInfo = operatorInfo;
1038
1379
  return Rule;
1039
1380
  }());
1040
1381
 
@@ -5526,6 +5867,6 @@ var Heatmap = /** @class */ (function (_super) {
5526
5867
  /**
5527
5868
  * The current version of the Flocc library.
5528
5869
  */
5529
- var version = "0.5.22";
5870
+ var version = "0.5.23";
5530
5871
 
5531
5872
  export { ASCIIRenderer, Agent, CanvasRenderer, Colors, Environment, GridEnvironment, Heatmap, Histogram, KDTree, LineChartRenderer, Network, NumArray, Rule, TableRenderer, Terrain, version as VERSION, Vector, utils };
package/dist/flocc.js CHANGED
@@ -823,7 +823,37 @@
823
823
  Operators["agent"] = "agent";
824
824
  Operators["environment"] = "environment";
825
825
  Operators["vector"] = "vector";
826
+ Operators["log"] = "log";
826
827
  })(Operators || (Operators = {}));
828
+ var operatorInfo = {
829
+ add: { min: 2, max: 2 },
830
+ subtract: { min: 2, max: 2 },
831
+ multiply: { min: 2, max: 2 },
832
+ divide: { min: 2, max: 2 },
833
+ mod: { min: 2, max: 2 },
834
+ power: { min: 2, max: 2 },
835
+ get: { min: 1, max: 1 },
836
+ set: { min: 2, max: 2 },
837
+ enqueue: { min: 2, max: 2 },
838
+ local: { min: 1, max: 2 },
839
+ if: { min: 2, max: 3 },
840
+ and: { min: 2, max: 2 },
841
+ or: { min: 2, max: 2 },
842
+ gt: { min: 2, max: 2 },
843
+ gte: { min: 2, max: 2 },
844
+ lt: { min: 2, max: 2 },
845
+ lte: { min: 2, max: 2 },
846
+ eq: { min: 2, max: 2 },
847
+ map: { min: 2, max: 2 },
848
+ filter: { min: 2, max: 2 },
849
+ key: { min: 2, max: 2 },
850
+ method: { min: 2, max: Infinity },
851
+ agent: { min: 0, max: 0 },
852
+ environment: { min: 0, max: 0 },
853
+ vector: { min: 1, max: Infinity },
854
+ log: { min: 1, max: Infinity }
855
+ };
856
+ var ARITHMETIC_OPS = new Set(["add", "subtract", "multiply", "divide", "mod", "power"]);
827
857
  var add = function (a, b) { return a + b; };
828
858
  var subtract = function (a, b) { return a - b; };
829
859
  var multiply = function (a, b) { return a * b; };
@@ -847,6 +877,127 @@
847
877
  return null;
848
878
  return obj[name].apply(obj, args);
849
879
  };
880
+ /**
881
+ * Format a single value for step display. If isFirst is true and the value is
882
+ * a known operator string, render it bare (unquoted).
883
+ */
884
+ function formatStepValue(val, isFirst) {
885
+ if (val === null || val === undefined)
886
+ return String(val);
887
+ if (typeof val === "number" || typeof val === "boolean")
888
+ return String(val);
889
+ if (typeof val === "string") {
890
+ if (isFirst && val in Operators)
891
+ return val;
892
+ return JSON.stringify(val);
893
+ }
894
+ if (val instanceof Array) {
895
+ if (val.length === 0)
896
+ return "[]";
897
+ var parts = val.map(function (s, i) { return formatStepValue(s, i === 0); });
898
+ return "(" + parts.join(" ") + ")";
899
+ }
900
+ if (typeof val === "object") {
901
+ try {
902
+ return JSON.stringify(val);
903
+ }
904
+ catch (_a) {
905
+ return String(val);
906
+ }
907
+ }
908
+ return String(val);
909
+ }
910
+ /**
911
+ * Format a step into a readable string representation for logging.
912
+ */
913
+ function formatStep(step) {
914
+ return formatStepValue(step, false);
915
+ }
916
+ /**
917
+ * Format a value as an S-expression atom (no recursion into arrays).
918
+ */
919
+ function formatAtom(val) {
920
+ if (val === null || val === undefined)
921
+ return "null";
922
+ if (typeof val === "number" || typeof val === "boolean")
923
+ return String(val);
924
+ if (typeof val === "string")
925
+ return JSON.stringify(val);
926
+ if (typeof val === "object" && !Array.isArray(val)) {
927
+ try {
928
+ return JSON.stringify(val);
929
+ }
930
+ catch (_a) {
931
+ return "[object]";
932
+ }
933
+ }
934
+ return String(val);
935
+ }
936
+ /**
937
+ * Format a value for S-expression display. The first element (operator) of
938
+ * a step is rendered as a bare identifier; other string values are quoted.
939
+ */
940
+ function formatSexpValue(val, isOperator) {
941
+ if (isOperator && typeof val === "string")
942
+ return val;
943
+ return formatAtom(val);
944
+ }
945
+ /**
946
+ * Pretty-print a single step as an S-expression with width-based wrapping.
947
+ */
948
+ function prettyFormatStep(step, indentUnit, maxLineWidth, currentIndent) {
949
+ if (!Array.isArray(step))
950
+ return formatAtom(step);
951
+ if (step.length === 0)
952
+ return "()";
953
+ // Try compact first
954
+ var compact = "(" + step.map(function (s, i) {
955
+ if (Array.isArray(s))
956
+ return prettyFormatStep(s, indentUnit, Infinity, "");
957
+ return formatSexpValue(s, i === 0);
958
+ }).join(" ") + ")";
959
+ if (currentIndent.length + compact.length <= maxLineWidth)
960
+ return compact;
961
+ // Wrap: operator on first line, each arg indented
962
+ var op = Array.isArray(step[0]) ? prettyFormatStep(step[0], indentUnit, maxLineWidth, currentIndent) : formatSexpValue(step[0], true);
963
+ var args = step.slice(1);
964
+ if (args.length === 0)
965
+ return "(" + op + ")";
966
+ var childIndent = currentIndent + indentUnit;
967
+ var formattedArgs = args.map(function (a) { return prettyFormatStep(a, indentUnit, maxLineWidth, childIndent); });
968
+ return "(" + op + "\n" + formattedArgs.map(function (a) { return childIndent + a; }).join("\n") + ")";
969
+ }
970
+ function validOperatorNames() {
971
+ return Object.keys(operatorInfo).join(", ");
972
+ }
973
+ function checkArity(op, argCount, strict) {
974
+ if (strict === void 0) { strict = false; }
975
+ var info = operatorInfo[op];
976
+ if (!info)
977
+ return null;
978
+ if (info.min === info.max) {
979
+ if (argCount < info.min) {
980
+ return "Rule: \"" + op + "\" expects " + info.min + " argument" + (info.min !== 1 ? "s" : "") + ", got " + argCount;
981
+ }
982
+ if (strict && argCount > info.max) {
983
+ return "Rule: \"" + op + "\" expects " + info.min + " argument" + (info.min !== 1 ? "s" : "") + ", got " + argCount;
984
+ }
985
+ }
986
+ else if (info.max === Infinity) {
987
+ if (argCount < info.min) {
988
+ return "Rule: \"" + op + "\" expects at least " + info.min + " argument" + (info.min !== 1 ? "s" : "") + ", got " + argCount;
989
+ }
990
+ }
991
+ else {
992
+ if (argCount < info.min) {
993
+ return "Rule: \"" + op + "\" expects " + info.min + "-" + info.max + " arguments, got " + argCount;
994
+ }
995
+ if (strict && argCount > info.max) {
996
+ return "Rule: \"" + op + "\" expects " + info.min + "-" + info.max + " arguments, got " + argCount;
997
+ }
998
+ }
999
+ return null;
1000
+ }
850
1001
  /**
851
1002
  * The `Rule` class is an experimental interface for adding behavior to {@linkcode Agent}s. A `Rule` object may be used in place of a `tick` function to be added as `Agent` behavior using `agent.set('tick', tickRule)`. As a trivial example, consider the following `Rule`, which increments the `Agent`'s `x` value with every time step:
852
1003
  *
@@ -901,6 +1052,7 @@
901
1052
  * |`"agent"`|`0`|No arguments; returns the `Agent`|
902
1053
  * |`"environment"`|`0`|No arguments, returns the `Environment`|
903
1054
  * |`"vector"`|`any`|Creates an n-dimensional {@linkcode Vector} from the supplied arguments|
1055
+ * |`"log"`|`any`|Logs expression(s) and returns the last evaluated value|
904
1056
  */
905
1057
  function Rule(environment, steps) {
906
1058
  var _this = this;
@@ -909,10 +1061,12 @@
909
1061
  /** @hidden */
910
1062
  this.locals = {};
911
1063
  /**
912
- * interpret single array step
913
- * @since 0.3.0
914
- * @hidden
1064
+ * When true, evaluation traces are logged and captured.
915
1065
  */
1066
+ this.trace = false;
1067
+ this.traceLog = [];
1068
+ /** @hidden */
1069
+ this._traceDepth = 0;
916
1070
  this.evaluate = function (agent, step) {
917
1071
  var first = step && step.length > 0 ? step[0] : null;
918
1072
  if (first === undefined || first === null)
@@ -923,30 +1077,79 @@
923
1077
  return _this.evaluate(agent, step.slice(1));
924
1078
  return innerStep;
925
1079
  }
926
- if (first === "log") {
927
- console.log("logging", step.slice(1), _this.evaluate(agent, step.slice(1)));
928
- return null;
1080
+ // Trace: check if this is a known operator call to trace
1081
+ var shouldTrace = _this.trace && typeof first === "string" && first in Operators;
1082
+ if (shouldTrace)
1083
+ _this._traceDepth++;
1084
+ var result = _this._evaluateOp(agent, step, first);
1085
+ if (shouldTrace) {
1086
+ _this._traceLogEntry(step, result);
1087
+ _this._traceDepth--;
1088
+ }
1089
+ return result;
1090
+ };
1091
+ /** @hidden */
1092
+ this._evaluateOp = function (agent, step, first) {
1093
+ if (first === Operators.log) {
1094
+ var args = step.slice(1);
1095
+ if (args.length === 0) {
1096
+ console.warn("Rule: \"log\" expects at least 1 argument, got 0");
1097
+ return null;
1098
+ }
1099
+ var lastValue = null;
1100
+ for (var i = 0; i < args.length; i++) {
1101
+ var expr = args[i];
1102
+ var evaluated = _this.evaluate(agent, [expr]);
1103
+ console.log("Rule log: " + formatStep(expr) + " \u2192 " + formatStep(evaluated));
1104
+ lastValue = evaluated;
1105
+ }
1106
+ return lastValue;
929
1107
  }
930
1108
  if (!(first in Operators) && step.length > 1) {
1109
+ if (typeof first === "string") {
1110
+ console.warn("Rule: unknown operator \"" + first + "\". Valid operators: " + validOperatorNames());
1111
+ }
931
1112
  return step;
932
1113
  }
1114
+ var argCount = step.length - 1;
1115
+ // Arity check at runtime for known operators
1116
+ if (typeof first === "string" && first in Operators) {
1117
+ var arityMsg = checkArity(first, argCount);
1118
+ if (arityMsg) {
1119
+ console.warn(arityMsg);
1120
+ }
1121
+ }
933
1122
  var a = step.length > 1 ? [step[1]] : undefined;
934
1123
  var b = step.length > 2 ? [step[2]] : undefined;
935
1124
  var c = step.length > 3 ? [step[3]] : undefined;
936
- if (first === Operators.add)
937
- return add(_this.evaluate(agent, a), _this.evaluate(agent, b));
938
- if (first === Operators.subtract)
939
- return subtract(_this.evaluate(agent, a), _this.evaluate(agent, b));
940
- if (first === Operators.multiply)
941
- return multiply(_this.evaluate(agent, a), _this.evaluate(agent, b));
942
- if (first === Operators.divide)
943
- return divide(_this.evaluate(agent, a), _this.evaluate(agent, b));
944
- if (first === Operators.mod)
945
- return mod(_this.evaluate(agent, a), _this.evaluate(agent, b));
946
- if (first === Operators.power)
947
- return power(_this.evaluate(agent, a), _this.evaluate(agent, b));
948
- if (first === Operators.get)
949
- return get(agent, _this.evaluate(agent, a));
1125
+ // Arithmetic operators with type checking
1126
+ if (ARITHMETIC_OPS.has(first)) {
1127
+ var va = _this.evaluate(agent, a);
1128
+ var vb = _this.evaluate(agent, b);
1129
+ if (typeof va !== "number" || typeof vb !== "number") {
1130
+ console.warn("Rule: \"" + first + "\" expects numeric arguments, got " + typeof va + " and " + typeof vb);
1131
+ }
1132
+ if (first === Operators.add)
1133
+ return add(va, vb);
1134
+ if (first === Operators.subtract)
1135
+ return subtract(va, vb);
1136
+ if (first === Operators.multiply)
1137
+ return multiply(va, vb);
1138
+ if (first === Operators.divide)
1139
+ return divide(va, vb);
1140
+ if (first === Operators.mod)
1141
+ return mod(va, vb);
1142
+ if (first === Operators.power)
1143
+ return power(va, vb);
1144
+ }
1145
+ if (first === Operators.get) {
1146
+ var keyName = _this.evaluate(agent, a);
1147
+ var result = get(agent, keyName);
1148
+ if (result === null || result === undefined) {
1149
+ console.warn("Rule: \"get\" returned null for key \"" + keyName + "\" \u2014 key may not exist on agent");
1150
+ }
1151
+ return result;
1152
+ }
950
1153
  if (first === Operators.set)
951
1154
  return set(agent, _this.evaluate(agent, a), _this.evaluate(agent, b));
952
1155
  if (first === Operators.enqueue) {
@@ -1034,13 +1237,151 @@
1034
1237
  this.environment = environment;
1035
1238
  this.steps = steps;
1036
1239
  }
1240
+ /**
1241
+ * Format arbitrary steps as pretty-printed S-expressions.
1242
+ */
1243
+ Rule.formatSteps = function (steps, options) {
1244
+ var indent = (options && options.indent) || " ";
1245
+ var maxLineWidth = (options && options.maxLineWidth) || 60;
1246
+ // Check if it's multi-step (array of arrays)
1247
+ var isMultiStep = steps.length > 0 && Array.isArray(steps[0]);
1248
+ if (isMultiStep) {
1249
+ return steps.map(function (s) { return prettyFormatStep(s, indent, maxLineWidth, ""); }).join("\n\n");
1250
+ }
1251
+ return prettyFormatStep(steps, indent, maxLineWidth, "");
1252
+ };
1253
+ /**
1254
+ * Pretty-print the rule's step tree as S-expressions.
1255
+ */
1256
+ Rule.prototype.format = function (options) {
1257
+ return Rule.formatSteps(this.steps, options);
1258
+ };
1259
+ /**
1260
+ * Returns a formatted S-expression representation of the rule.
1261
+ */
1262
+ Rule.prototype.toString = function () {
1263
+ return this.format();
1264
+ };
1265
+ /**
1266
+ * Validate the rule's step tree and return an array of diagnostics.
1267
+ * Does not throw — returns diagnostics for inspection.
1268
+ * @since 0.6.0
1269
+ */
1270
+ Rule.prototype.validate = function () {
1271
+ var diagnostics = [];
1272
+ if (!this.steps || (Array.isArray(this.steps) && this.steps.length === 0)) {
1273
+ diagnostics.push({
1274
+ path: "root",
1275
+ level: "warning",
1276
+ message: "Empty steps array"
1277
+ });
1278
+ return diagnostics;
1279
+ }
1280
+ // Check if steps is a single step (first element is a string operator)
1281
+ // or an array of steps (first element is an array)
1282
+ var isMultiStep = Array.isArray(this.steps[0]);
1283
+ if (isMultiStep) {
1284
+ // Multiple top-level steps
1285
+ for (var i = 0; i < this.steps.length; i++) {
1286
+ var step = this.steps[i];
1287
+ if (!Array.isArray(step)) {
1288
+ diagnostics.push({
1289
+ path: "[" + i + "]",
1290
+ level: "warning",
1291
+ message: "Step is a bare value (" + typeof step + ": " + JSON.stringify(step) + ") instead of an array"
1292
+ });
1293
+ }
1294
+ else {
1295
+ this._validateStep(step, "[" + i + "]", diagnostics);
1296
+ }
1297
+ }
1298
+ }
1299
+ else {
1300
+ // Single step (the steps array IS the step)
1301
+ this._validateStep(this.steps, "root", diagnostics);
1302
+ }
1303
+ return diagnostics;
1304
+ };
1305
+ /** @hidden */
1306
+ Rule.prototype._validateStep = function (step, path, diagnostics) {
1307
+ if (step.length === 0) {
1308
+ diagnostics.push({
1309
+ path: path,
1310
+ level: "warning",
1311
+ message: "Empty step array"
1312
+ });
1313
+ return;
1314
+ }
1315
+ var first = step[0];
1316
+ // If first element is an array, it's a nested step sequence
1317
+ if (Array.isArray(first)) {
1318
+ this._validateStep(first, path + "[0]", diagnostics);
1319
+ for (var i = 1; i < step.length; i++) {
1320
+ if (Array.isArray(step[i])) {
1321
+ this._validateStep(step[i], path + ("[" + i + "]"), diagnostics);
1322
+ }
1323
+ }
1324
+ return;
1325
+ }
1326
+ if (typeof first === "string") {
1327
+ var argCount = step.length - 1;
1328
+ // Check if operator is known
1329
+ if (!(first in Operators)) {
1330
+ if (step.length > 1) {
1331
+ diagnostics.push({
1332
+ path: path,
1333
+ level: "error",
1334
+ message: "Unknown operator \"" + first + "\". Valid operators: " + validOperatorNames()
1335
+ });
1336
+ }
1337
+ return;
1338
+ }
1339
+ // Check arity (strict: also warn on too many args)
1340
+ var arityMsg = checkArity(first, argCount, true);
1341
+ if (arityMsg) {
1342
+ diagnostics.push({
1343
+ path: path,
1344
+ level: "error",
1345
+ message: arityMsg
1346
+ });
1347
+ }
1348
+ // Recurse into sub-steps
1349
+ for (var i = 1; i < step.length; i++) {
1350
+ if (Array.isArray(step[i])) {
1351
+ this._validateStep(step[i], path + ("[" + i + "]"), diagnostics);
1352
+ }
1353
+ }
1354
+ }
1355
+ };
1356
+ /**
1357
+ * interpret single array step
1358
+ * @since 0.3.0
1359
+ * @hidden
1360
+ */
1361
+ /** @hidden */
1362
+ Rule.prototype._traceLogEntry = function (step, result) {
1363
+ var indent = " ".repeat(this._traceDepth - 1);
1364
+ var formatted = formatStep(step);
1365
+ var resultFormatted = formatStep(result);
1366
+ var line = "Rule trace: " + indent + formatted + " \u2192 " + resultFormatted;
1367
+ console.log(line);
1368
+ this.traceLog.push(line);
1369
+ };
1037
1370
  /**
1038
1371
  * @since 0.3.0
1039
1372
  * @hidden
1040
1373
  */
1041
1374
  Rule.prototype.call = function (agent) {
1375
+ if (this.trace) {
1376
+ this.traceLog = [];
1377
+ this._traceDepth = 0;
1378
+ }
1042
1379
  return this.evaluate(agent, this.steps);
1043
1380
  };
1381
+ /**
1382
+ * Static operator info mapping operator names to their expected argument counts.
1383
+ */
1384
+ Rule.operatorInfo = operatorInfo;
1044
1385
  return Rule;
1045
1386
  }());
1046
1387
 
@@ -5532,7 +5873,7 @@
5532
5873
  /**
5533
5874
  * The current version of the Flocc library.
5534
5875
  */
5535
- var version = "0.5.22";
5876
+ var version = "0.5.23";
5536
5877
 
5537
5878
  exports.ASCIIRenderer = ASCIIRenderer;
5538
5879
  exports.Agent = Agent;
@@ -3,6 +3,23 @@ import { Agent } from "../agents/Agent";
3
3
  declare type StepToken = number | string | Step;
4
4
  interface Step extends Array<StepToken> {
5
5
  }
6
+ interface RuleDiagnostic {
7
+ path: string;
8
+ level: "error" | "warning";
9
+ message: string;
10
+ }
11
+ interface RuleFormatOptions {
12
+ indent?: string;
13
+ maxLineWidth?: number;
14
+ }
15
+ /**
16
+ * Operator info: maps operator names to their expected argument counts.
17
+ * `min` and `max` define the valid range. If `max` is Infinity, the operator is variadic.
18
+ */
19
+ interface OperatorArity {
20
+ min: number;
21
+ max: number;
22
+ }
6
23
  /**
7
24
  * The `Rule` class is an experimental interface for adding behavior to {@linkcode Agent}s. A `Rule` object may be used in place of a `tick` function to be added as `Agent` behavior using `agent.set('tick', tickRule)`. As a trivial example, consider the following `Rule`, which increments the `Agent`'s `x` value with every time step:
8
25
  *
@@ -36,6 +53,23 @@ declare class Rule {
36
53
  locals: {
37
54
  [key: string]: any;
38
55
  };
56
+ /**
57
+ * When true, evaluation traces are logged and captured.
58
+ */
59
+ trace: boolean;
60
+ traceLog: string[];
61
+ /** @hidden */
62
+ private _traceDepth;
63
+ /**
64
+ * Static operator info mapping operator names to their expected argument counts.
65
+ */
66
+ static operatorInfo: {
67
+ [key: string]: OperatorArity;
68
+ };
69
+ /**
70
+ * Format arbitrary steps as pretty-printed S-expressions.
71
+ */
72
+ static formatSteps(steps: any[], options?: RuleFormatOptions): string;
39
73
  /**
40
74
  * A single step may be as simple as `["get", "x"]`. This returns the `Agent`'s `"x"` value to the outer step that contains it. So, for example, the step `["add", 1, ["get", "x"]]`, working from the inside out, retrieves the `"x"` value and then adds `1` to it. More complex steps function similarly, always traversing to the deepest nested step, evaluating it, and 'unwrapping' until all steps have been executed.
41
75
  *
@@ -67,18 +101,39 @@ declare class Rule {
67
101
  * |`"agent"`|`0`|No arguments; returns the `Agent`|
68
102
  * |`"environment"`|`0`|No arguments, returns the `Environment`|
69
103
  * |`"vector"`|`any`|Creates an n-dimensional {@linkcode Vector} from the supplied arguments|
104
+ * |`"log"`|`any`|Logs expression(s) and returns the last evaluated value|
70
105
  */
71
106
  constructor(environment: Environment, steps: Step[]);
107
+ /**
108
+ * Pretty-print the rule's step tree as S-expressions.
109
+ */
110
+ format(options?: RuleFormatOptions): string;
111
+ /**
112
+ * Returns a formatted S-expression representation of the rule.
113
+ */
114
+ toString(): string;
115
+ /**
116
+ * Validate the rule's step tree and return an array of diagnostics.
117
+ * Does not throw — returns diagnostics for inspection.
118
+ * @since 0.6.0
119
+ */
120
+ validate(): RuleDiagnostic[];
121
+ /** @hidden */
122
+ private _validateStep;
72
123
  /**
73
124
  * interpret single array step
74
125
  * @since 0.3.0
75
126
  * @hidden
76
127
  */
128
+ /** @hidden */
129
+ private _traceLogEntry;
77
130
  evaluate: (agent: Agent, step: any[]) => any;
131
+ /** @hidden */
132
+ private _evaluateOp;
78
133
  /**
79
134
  * @since 0.3.0
80
135
  * @hidden
81
136
  */
82
137
  call(agent: Agent): any;
83
138
  }
84
- export { Rule };
139
+ export { Rule, RuleDiagnostic, RuleFormatOptions };
package/dist/main.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { Agent } from "./agents/Agent";
2
2
  export { Vector } from "./helpers/Vector";
3
3
  export { Network } from "./helpers/Network";
4
- export { Rule } from "./helpers/Rule";
4
+ export { Rule, RuleDiagnostic, RuleFormatOptions } from "./helpers/Rule";
5
5
  export { KDTree } from "./helpers/KDTree";
6
6
  export { NumArray } from "./helpers/NumArray";
7
7
  export { Colors, Terrain } from "./helpers/Terrain";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flocc",
3
- "version": "0.5.22",
3
+ "version": "0.5.23",
4
4
  "description": "Agent-based modeling in JavaScript in the browser or server.",
5
5
  "main": "dist/flocc.js",
6
6
  "module": "dist/flocc.es.js",