eyeling 1.19.3 → 1.19.5

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/HANDBOOK.md CHANGED
@@ -2955,104 +2955,50 @@ That is why the result is 100%.
2955
2955
 
2956
2956
  ## Appendix F — The ARC approach: Answer • Reason Why • Check
2957
2957
 
2958
- A useful way to structure an Eyeling program is the ARC approach:
2958
+ A simple way to write a good Eyeling program is to make it do three things in one file:
2959
2959
 
2960
- > Answer Reason Why Check
2960
+ > give the answer, say why, and check that it really holds.
2961
2961
 
2962
- The idea is simple: do not stop at producing a result. Produce the result, explain why it is the right result, and include a concrete verification that fails loudly when an important assumption does not hold.
2962
+ That is the ARC approach: **Answer Reason Why Check**.
2963
2963
 
2964
- ARC treats a program as a small, accountable artifact built from three ingredients:
2964
+ The idea is not to make the program more grand or formal. It is to make it more useful. A bare result is often not enough. A reader also wants to see the small reason that matters, and to know that the program will fail loudly if an important assumption is wrong.
2965
2965
 
2966
- 1. Data
2967
- 2. Logic
2968
- 3. A Question
2966
+ In Eyeling this style comes quite naturally. Facts hold the data. Rules derive the conclusion. `log:outputString` can turn the conclusion into readable output. And a rule that concludes `false` acts as a fuse: if a bad condition becomes provable, the run stops instead of quietly producing a misleading result.
2969
2967
 
2970
- From these, we build a compact program that answers the question, explains the answer, and checks itself.
2968
+ ### F.1 What the three parts mean
2971
2969
 
2972
- ### F.1 The three parts
2970
+ The **Answer** is the direct result. It should be short and easy to recognize. In many Eyeling files it is a final recommendation, a route, a computed value, a decision such as `allowed` or `blocked`, or a small report line emitted with `log:outputString`.
2973
2971
 
2974
- #### Answer
2972
+ The **Reason Why** is the compact explanation. It is not hidden chain-of-thought and it does not need to be long. Usually it is just the witness, threshold, policy, path, or intermediate fact that made the answer follow. A good reason tells the reader what mattered.
2975
2973
 
2976
- The **Answer** is the direct response to the question being asked.
2974
+ The **Check** is the part that keeps the program honest. It should do more than repeat the answer in different words. A good check tests something that could really fail: a structural invariant, a recomputed quantity, a boundary condition, or a rule that derives `false` when the answer would be inconsistent with the inputs.
2977
2975
 
2978
- It should be short, specific, and easy to read. In Eyeling, this is often emitted as one or more `log:outputString` lines such as:
2976
+ A short way to remember ARC is this:
2979
2977
 
2980
- - the final decision
2981
- - the selected item
2982
- - the computed value
2983
- - the resulting classification
2978
+ > an answer tells you **what** happened, a reason tells you **why**, and a check tells you **whether you should trust it**.
2984
2979
 
2985
- A good Answer reads like something you could show to a user, an auditor, or a calling program.
2980
+ ### F.2 Why this fits Eyeling well
2986
2981
 
2987
- #### Reason Why
2982
+ ARC is not an extra subsystem in Eyeling. It is mostly a good habit.
2988
2983
 
2989
- The **Reason Why** explains why the Answer is correct.
2984
+ Eyeling already separates data from logic. It already lets you derive readable output instead of printing ad hoc text during proof search. And it already has a very strong notion of validation through inference fuses. So ARC is really just a clean way to organize an ordinary Eyeling file so that a human reader can see the result, the explanation, and the safety net together.
2990
2985
 
2991
- This is not a full proof calculus or a hidden chain of thought. It is a concise, inspectable explanation grounded in the facts, rules, thresholds, identities, or policies that matter for the case. In practice, it often includes:
2986
+ This is especially useful for examples. A newcomer can run the file and see what it does. A maintainer can inspect the few rules that justify the result. And an external developer can tell whether the example merely prints something nice or actually checks itself.
2992
2987
 
2993
- - the relevant inputs
2994
- - the governing rule or policy
2995
- - the key intermediate facts
2996
- - the condition that made the conclusion follow
2988
+ ### F.3 A simple pattern to follow
2997
2989
 
2998
- In Eyeling, the Reason Why is usually rendered as additional `log:outputString` lines derived from the same closure as the Answer.
2990
+ A practical ARC-style Eyeling file often has four visible layers.
2999
2991
 
3000
- #### Check
2992
+ First come the **facts**: the input data, parameters, thresholds, policies, or known relationships. Then comes the **logic**: the rules that derive the internal conclusion. Then comes the **presentation**: rules that turn the result into `log:outputString` lines or other report facts. Finally come the **checks**: rules that validate the result or trigger `false` when an invariant is broken.
3001
2993
 
3002
- The **Check** is an independent validation step.
2994
+ You do not have to separate these layers perfectly, but it helps a lot when the file reads in roughly that order.
3003
2995
 
3004
- Its purpose is not to restate the Answer, but to test that important invariants still hold. A Check should fail loudly if the program’s assumptions break, if a data dependency is malformed, or if an edge case invalidates the intended conclusion.
3005
-
3006
- In Eyeling, Checks are a natural fit for either:
3007
-
3008
- - derived facts such as `:ok :signatureVerified true .`, or
3009
- - inference fuses such as `{ ... } => false .` when a violation must stop execution.
3010
-
3011
- This makes verification part of the program itself rather than something left to external commentary.
3012
-
3013
- ### F.2 Proof = Reason Why + Check
3014
-
3015
- ARC summarizes its trust model as:
3016
-
3017
- > Proof = Reason Why + Check
3018
-
3019
- That is a practical notion of proof. The Reason Why explains the logic in human terms. The Check verifies that the critical conditions actually hold at runtime.
3020
-
3021
- For many real workflows, that combination is more useful than a bare result: it is inspectable, repeatable, and suitable for automation.
3022
-
3023
- ### F.3 Why ARC fits Eyeling well
3024
-
3025
- Eyeling already encourages the separation that ARC needs.
3026
-
3027
- Rules derive facts. Facts can include output facts. Output is not printed eagerly during proof search; instead, `log:outputString` facts are collected from the final closure and rendered deterministically whenever they are present. This makes it natural to derive a structured Answer and Reason Why as part of the logic itself.
3028
-
3029
- Checks also map well to Eyeling. A rule with conclusion `false` acts as an inference fuse: if its body becomes provable, execution stops with a hard failure. This is exactly the behavior we want for “must-hold” conditions.
3030
-
3031
- So ARC in Eyeling is not an add-on. It is mostly a disciplined way of organizing what Eyeling already does well: derive conclusions, expose supporting facts, and enforce invariants.
3032
-
3033
- ### F.4 A practical pattern
3034
-
3035
- A simple ARC-oriented Eyeling file often has four layers:
3036
-
3037
- 1. **Facts**
3038
- Input data, parameters, policies, and known relationships.
3039
-
3040
- 2. **Logic**
3041
- Rules that derive the program’s internal conclusions.
3042
-
3043
- 3. **Presentation**
3044
- Rules that turn derived conclusions into `log:outputString` lines for the Answer and Reason Why.
3045
-
3046
- 4. **Verification**
3047
- Rules that derive check facts or trigger inference fuses on violations.
3048
-
3049
- A useful habit is to keep these layers visually separate in the file.
3050
-
3051
- ### F.5 A tiny template
2996
+ ### F.4 A tiny template
3052
2997
 
3053
2998
  ```n3
3054
2999
  @prefix : <http://example.org/> .
3055
3000
  @prefix log: <http://www.w3.org/2000/10/swap/log#> .
3001
+ @prefix math: <http://www.w3.org/2000/10/swap/math#> .
3056
3002
 
3057
3003
  # Facts
3058
3004
  :case :input 42 .
@@ -3078,25 +3024,38 @@ A useful habit is to keep these layers visually separate in the file.
3078
3024
  => false .
3079
3025
  ```
3080
3026
 
3081
- The exact presentation style can vary, but the shape remains the same: derive the result, explain the result, and verify the result.
3027
+ The exact wording can vary. The important thing is the shape: derive the result, make the key reason visible, and include at least one check that could fail for a real reason.
3028
+
3029
+ ### F.5 What a good check looks like
3030
+
3031
+ A good check is not a decorative `:ok true` line. It should add real confidence.
3032
+
3033
+ Sometimes that means recomputing a quantity from another angle. Sometimes it means checking a witness path instead of only the summary result. Sometimes it means making sure a threshold really was crossed, or that a list or graph has the shape the rest of the program assumes. And sometimes the right check is simply an inference fuse that says: if this contradiction appears, stop.
3034
+
3035
+ The point is not to make checks large. The point is to make them real.
3036
+
3037
+ ### F.6 Examples in `examples/` that read well in ARC style
3038
+
3039
+ The following examples are especially good places to see this style in practice.
3040
+
3041
+ - [`examples/delfour.n3`](examples/delfour.n3) — privacy-preserving shopping assistance with a concrete recommendation, an explanation, and policy checks. Expected output: [`examples/output/delfour.n3`](examples/output/delfour.n3)
3042
+ - [`examples/control-system.n3`](examples/control-system.n3) — derives actuator decisions, explains the control basis, and checks the result. Expected output: [`examples/output/control-system.n3`](examples/output/control-system.n3)
3043
+ - [`examples/deep-taxonomy-100000.n3`](examples/deep-taxonomy-100000.n3) — a deep classification stress test whose answer is whether the final goal class is reached. Expected output: [`examples/output/deep-taxonomy-100000.n3`](examples/output/deep-taxonomy-100000.n3)
3044
+ - [`examples/gps.n3`](examples/gps.n3) — route planning with readable route output. Expected output: [`examples/output/gps.n3`](examples/output/gps.n3)
3045
+ - [`examples/sudoku.n3`](examples/sudoku.n3) — solver output plus legality and consistency checks. Expected output: [`examples/output/sudoku.n3`](examples/output/sudoku.n3)
3082
3046
 
3083
- ### F.6 What ARC is not
3047
+ ### F.7 How to read an ARC-style example
3084
3048
 
3085
- ARC does **not** mean:
3049
+ A good way to read one of these files is to start with the question in the comments or input facts. Then find the part that gives the answer. Then trace the few rules that explain why that answer follows. Finally, look for the checks: the validation facts, the recomputation, or the `=> false` fuse that would stop the run if something important were wrong.
3086
3050
 
3087
- - printing a result and calling it explained
3088
- - replacing checks with prose
3089
- - hiding the important assumptions
3090
- - relying on “trust me” comments outside the executable artifact
3051
+ That reading order keeps the example grounded in observable behavior rather than in source code alone.
3091
3052
 
3092
- A file follows ARC only when the answer, explanation, and validation are all carried by the program itself.
3053
+ ### F.8 What ARC is not
3093
3054
 
3094
- ### F.7 A good default for examples
3055
+ ARC does not mean wrapping every file in ceremony. It does not mean long prose explanations. It does not mean hiding important assumptions in comments while the executable part stays thin. And it does not mean replacing checks with a confident tone.
3095
3056
 
3096
- For worked examples in this handbook, ARC is a strong default presentation style:
3057
+ A file really follows ARC only when the answer, the explanation, and the validation all live in the program itself.
3097
3058
 
3098
- - **Answer** for the main result
3099
- - **Reason Why** for the key supporting explanation
3100
- - **Check** for invariants and fail-loud validation
3059
+ ### F.9 Why this style is worth using
3101
3060
 
3102
- This keeps examples readable for newcomers while also making them more useful as reusable, auditable logic artifacts.
3061
+ This style is worth using because it makes an Eyeling file easier to run, easier to inspect, and easier to trust. The result is visible. The key reason is visible. The check is visible. That makes examples better teaching material, makes policy or computation examples easier to audit, and makes the whole file more reusable as a small reasoning artifact instead of an opaque session transcript.
@@ -23,6 +23,7 @@
23
23
  10 :fibonacci ?F10.
24
24
  100 :fibonacci ?F100.
25
25
  1000 :fibonacci ?F1000.
26
+ 10000 :fibonacci ?F10000.
26
27
  } => {
27
28
  :test :is {
28
29
  0 :fibonacci ?F0.
@@ -30,6 +31,7 @@
30
31
  10 :fibonacci ?F10.
31
32
  100 :fibonacci ?F100.
32
33
  1000 :fibonacci ?F1000.
34
+ 10000 :fibonacci ?F10000.
33
35
  }.
34
36
  }.
35
37
 
@@ -6,4 +6,5 @@
6
6
  10 :fibonacci 55 .
7
7
  100 :fibonacci 354224848179261915075 .
8
8
  1000 :fibonacci 43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875 .
9
+ 10000 :fibonacci 33644764876431783266621612005107543310302148460680063906564769974680081442166662368155595513633734025582065332680836159373734790483865268263040892463056431887354544369559827491606602099884183933864652731300088830269235673613135117579297437854413752130520504347701602264758318906527890855154366159582987279682987510631200575428783453215515103870818298969791613127856265033195487140214287532698187962046936097879900350962302291026368131493195275630227837628441540360584402572114334961180023091208287046088923962328835461505776583271252546093591128203925285393434620904245248929403901706233888991085841065183173360437470737908552631764325733993712871937587746897479926305837065742830161637408969178426378624212835258112820516370298089332099905707920064367426202389783111470054074998459250360633560933883831923386783056136435351892133279732908133732642652633989763922723407882928177953580570993691049175470808931841056146322338217465637321248226383092103297701648054726243842374862411453093812206564914032751086643394517512161526545361333111314042436854805106765843493523836959653428071768775328348234345557366719731392746273629108210679280784718035329131176778924659089938635459327894523777674406192240337638674004021330343297496902028328145933418826817683893072003634795623117103101291953169794607632737589253530772552375943788434504067715555779056450443016640119462580972216729758615026968443146952034614932291105970676243268515992834709891284706740862008587135016260312071903172086094081298321581077282076353186624611278245537208532365305775956430072517744315051539600905168603220349163222640885248852433158051534849622434848299380905070483482449327453732624567755879089187190803662058009594743150052402532709746995318770724376825907419939632265984147498193609285223945039707165443156421328157688908058783183404917434556270520223564846495196112460268313970975069382648706613264507665074611512677522748621598642530711298441182622661057163515069260029861704945425047491378115154139941550671256271197133252763631939606902895650288268608362241082050562430701794976171121233066073310059947366875 .
9
10
  } .
package/eyeling.js CHANGED
@@ -2576,6 +2576,24 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
2576
2576
 
2577
2577
  // math:absoluteValue
2578
2578
  if (pv === MATH_NS + 'absoluteValue') {
2579
+ const ai = parseIntLiteral(g.s);
2580
+ if (ai !== null) {
2581
+ const outVal = ai < 0n ? -ai : ai;
2582
+ const lit = makeNumericOutputLiteral(outVal, XSD_INTEGER_DT);
2583
+
2584
+ if (g.o instanceof Var) {
2585
+ const s2 = { ...subst };
2586
+ s2[g.o.name] = lit;
2587
+ return [s2];
2588
+ }
2589
+ if (g.o instanceof Blank) return [{ ...subst }];
2590
+
2591
+ const oi = parseIntLiteral(g.o);
2592
+ if (oi !== null && oi === outVal) return [{ ...subst }];
2593
+ if (numEqualTerm(g.o, Number(outVal))) return [{ ...subst }];
2594
+ return [];
2595
+ }
2596
+
2579
2597
  const a = parseNum(g.s);
2580
2598
  if (a === null) return [];
2581
2599
 
@@ -2648,6 +2666,38 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
2648
2666
 
2649
2667
  // math:negation (inverse is itself)
2650
2668
  if (pv === MATH_NS + 'negation') {
2669
+ const si = parseIntLiteral(g.s);
2670
+ if (si !== null) {
2671
+ const outVal = -si;
2672
+ const lit = makeNumericOutputLiteral(outVal, XSD_INTEGER_DT);
2673
+ if (g.o instanceof Var) {
2674
+ const s2 = { ...subst };
2675
+ s2[g.o.name] = lit;
2676
+ return [s2];
2677
+ }
2678
+ if (g.o instanceof Blank) return [{ ...subst }];
2679
+ const oi = parseIntLiteral(g.o);
2680
+ if (oi !== null && oi === outVal) return [{ ...subst }];
2681
+ if (numEqualTerm(g.o, Number(outVal))) return [{ ...subst }];
2682
+ return [];
2683
+ }
2684
+
2685
+ const oi = parseIntLiteral(g.o);
2686
+ if (oi !== null) {
2687
+ const inVal = -oi;
2688
+ const lit = makeNumericOutputLiteral(inVal, XSD_INTEGER_DT);
2689
+ if (g.s instanceof Var) {
2690
+ const s2 = { ...subst };
2691
+ s2[g.s.name] = lit;
2692
+ return [s2];
2693
+ }
2694
+ if (g.s instanceof Blank) return [{ ...subst }];
2695
+ const si2 = parseIntLiteral(g.s);
2696
+ if (si2 !== null && si2 === inVal) return [{ ...subst }];
2697
+ if (numEqualTerm(g.s, Number(inVal))) return [{ ...subst }];
2698
+ return [];
2699
+ }
2700
+
2651
2701
  const neg = (x) => -x;
2652
2702
  return evalUnaryMathRel(g, subst, neg, neg);
2653
2703
  }
@@ -2706,6 +2756,25 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
2706
2756
  // Schema: $s+ math:rounded $o-
2707
2757
  // Note: spec says $o is xsd:integer, but we also accept any numeric $o that equals the rounded value.
2708
2758
  if (pv === MATH_NS + 'rounded') {
2759
+ const ai = parseIntLiteral(g.s);
2760
+ if (ai !== null) {
2761
+ const lit = makeNumericOutputLiteral(ai, XSD_INTEGER_DT);
2762
+
2763
+ if (g.o instanceof Var) {
2764
+ const s2 = { ...subst };
2765
+ s2[g.o.name] = lit;
2766
+ return [s2];
2767
+ }
2768
+ if (g.o instanceof Blank) return [{ ...subst }];
2769
+
2770
+ const oi = parseIntLiteral(g.o);
2771
+ if (oi !== null && oi === ai) return [{ ...subst }];
2772
+ if (numEqualTerm(g.o, Number(ai))) return [{ ...subst }];
2773
+
2774
+ const s2 = unifyTerm(g.o, lit, subst);
2775
+ return s2 !== null ? [s2] : [];
2776
+ }
2777
+
2709
2778
  const a = parseNum(g.s);
2710
2779
  if (a === null) return [];
2711
2780
  if (Number.isNaN(a)) return [];
@@ -8064,6 +8133,18 @@ function __printTriggeredFuse(rule, prefixes, subst /* optional */, extraNote /*
8064
8133
  }
8065
8134
  }
8066
8135
 
8136
+ function __exitReasoning(code, message /* optional */) {
8137
+ if (typeof process !== 'undefined' && process && typeof process.exit === 'function') {
8138
+ process.exit(code);
8139
+ return;
8140
+ }
8141
+
8142
+ const err = new Error(message || `Process exited with code ${code}`);
8143
+ err.__eyelingExit = true;
8144
+ err.code = code;
8145
+ throw err;
8146
+ }
8147
+
8067
8148
  function forwardChain(facts, forwardRules, backRules, onDerived /* optional */, opts = {}) {
8068
8149
  enterReasoningRun();
8069
8150
  try {
@@ -8203,7 +8284,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8203
8284
  // Allow dynamic fuses: ... => ?X. where ?X becomes false
8204
8285
  if (dynTerm instanceof Literal && dynTerm.value === 'false') {
8205
8286
  __printTriggeredFuse(r, opts && opts.prefixes, s, 'Dynamic head resolved to false.');
8206
- process.exit(2);
8287
+ __exitReasoning(2, 'Inference fuse triggered.');
8207
8288
  }
8208
8289
 
8209
8290
  const dynTriples = __graphTriplesOrTrue(dynTerm);
@@ -8231,10 +8312,10 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8231
8312
  const objIsGraph = obj instanceof GraphTerm;
8232
8313
  const subjIsTrue = subj instanceof Literal && subj.value === 'true';
8233
8314
  const objIsTrue = obj instanceof Literal && obj.value === 'true';
8315
+ const objIsFalse = obj instanceof Literal && obj.value === 'false';
8234
8316
 
8235
8317
  const isFwRuleTriple =
8236
- isLogImplies(instantiated.p) &&
8237
- ((subjIsGraph && objIsGraph) || (subjIsTrue && objIsGraph) || (subjIsGraph && objIsTrue));
8318
+ isLogImplies(instantiated.p) && (subjIsGraph || subjIsTrue) && (objIsGraph || objIsTrue || objIsFalse);
8238
8319
 
8239
8320
  const isBwRuleTriple =
8240
8321
  isLogImpliedBy(instantiated.p) &&
@@ -8249,47 +8330,39 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8249
8330
  changedHere = true;
8250
8331
  }
8251
8332
 
8252
- // Promote rule-producing triples to live rules, treating literal true as {}.
8253
- const left = __graphTriplesOrTrue(subj);
8254
- const right = __graphTriplesOrTrue(obj);
8255
-
8256
- if (left !== null && right !== null) {
8257
- if (isFwRuleTriple) {
8258
- const [premise, conclusion] = liftBlankRuleVars(left, right);
8259
- const headBlankLabels = collectBlankLabelsInTriples(conclusion);
8260
- const newRule = new Rule(premise, conclusion, true, false, headBlankLabels);
8261
- __prepareForwardRule(newRule);
8262
-
8263
- const key = __ruleKey(
8264
- newRule.isForward,
8265
- newRule.isFuse,
8266
- newRule.premise,
8267
- newRule.conclusion,
8268
- newRule.__dynamicConclusionTerm || null,
8269
- );
8270
- if (!forwardRules.__ruleKeySet.has(key)) {
8271
- forwardRules.__ruleKeySet.add(key);
8272
- forwardRules.push(newRule);
8273
- rulesChanged = true;
8274
- }
8275
- } else if (isBwRuleTriple) {
8276
- const [premise, conclusion] = liftBlankRuleVars(right, left);
8277
- const headBlankLabels = collectBlankLabelsInTriples(conclusion);
8278
- const newRule = new Rule(premise, conclusion, false, false, headBlankLabels);
8279
-
8280
- const key = __ruleKey(
8281
- newRule.isForward,
8282
- newRule.isFuse,
8283
- newRule.premise,
8284
- newRule.conclusion,
8285
- newRule.__dynamicConclusionTerm || null,
8286
- );
8287
- if (!backRules.__ruleKeySet.has(key)) {
8288
- backRules.__ruleKeySet.add(key);
8289
- backRules.push(newRule);
8290
- indexBackRule(backRules, newRule);
8291
- rulesChanged = true;
8292
- }
8333
+ // Promote rule-producing triples to live rules, treating literal true as {}
8334
+ // and literal false as a fuse head.
8335
+ if (isFwRuleTriple) {
8336
+ const newRule = __makeRuleFromTerms(subj, obj, true);
8337
+ __prepareForwardRule(newRule);
8338
+
8339
+ const key = __ruleKey(
8340
+ newRule.isForward,
8341
+ newRule.isFuse,
8342
+ newRule.premise,
8343
+ newRule.conclusion,
8344
+ newRule.__dynamicConclusionTerm || null,
8345
+ );
8346
+ if (!forwardRules.__ruleKeySet.has(key)) {
8347
+ forwardRules.__ruleKeySet.add(key);
8348
+ forwardRules.push(newRule);
8349
+ rulesChanged = true;
8350
+ }
8351
+ } else if (isBwRuleTriple) {
8352
+ const newRule = __makeRuleFromTerms(subj, obj, false);
8353
+
8354
+ const key = __ruleKey(
8355
+ newRule.isForward,
8356
+ newRule.isFuse,
8357
+ newRule.premise,
8358
+ newRule.conclusion,
8359
+ newRule.__dynamicConclusionTerm || null,
8360
+ );
8361
+ if (!backRules.__ruleKeySet.has(key)) {
8362
+ backRules.__ruleKeySet.add(key);
8363
+ backRules.push(newRule);
8364
+ indexBackRule(backRules, newRule);
8365
+ rulesChanged = true;
8293
8366
  }
8294
8367
  }
8295
8368
 
@@ -8372,7 +8445,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8372
8445
  // Inference fuse
8373
8446
  if (r.isFuse && sols.length) {
8374
8447
  __printTriggeredFuse(r, opts && opts.prefixes, sols[0]);
8375
- process.exit(2);
8448
+ __exitReasoning(2, 'Inference fuse triggered.');
8376
8449
  }
8377
8450
 
8378
8451
  for (const s of sols) {
package/lib/builtins.js CHANGED
@@ -2096,6 +2096,24 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
2096
2096
 
2097
2097
  // math:absoluteValue
2098
2098
  if (pv === MATH_NS + 'absoluteValue') {
2099
+ const ai = parseIntLiteral(g.s);
2100
+ if (ai !== null) {
2101
+ const outVal = ai < 0n ? -ai : ai;
2102
+ const lit = makeNumericOutputLiteral(outVal, XSD_INTEGER_DT);
2103
+
2104
+ if (g.o instanceof Var) {
2105
+ const s2 = { ...subst };
2106
+ s2[g.o.name] = lit;
2107
+ return [s2];
2108
+ }
2109
+ if (g.o instanceof Blank) return [{ ...subst }];
2110
+
2111
+ const oi = parseIntLiteral(g.o);
2112
+ if (oi !== null && oi === outVal) return [{ ...subst }];
2113
+ if (numEqualTerm(g.o, Number(outVal))) return [{ ...subst }];
2114
+ return [];
2115
+ }
2116
+
2099
2117
  const a = parseNum(g.s);
2100
2118
  if (a === null) return [];
2101
2119
 
@@ -2168,6 +2186,38 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
2168
2186
 
2169
2187
  // math:negation (inverse is itself)
2170
2188
  if (pv === MATH_NS + 'negation') {
2189
+ const si = parseIntLiteral(g.s);
2190
+ if (si !== null) {
2191
+ const outVal = -si;
2192
+ const lit = makeNumericOutputLiteral(outVal, XSD_INTEGER_DT);
2193
+ if (g.o instanceof Var) {
2194
+ const s2 = { ...subst };
2195
+ s2[g.o.name] = lit;
2196
+ return [s2];
2197
+ }
2198
+ if (g.o instanceof Blank) return [{ ...subst }];
2199
+ const oi = parseIntLiteral(g.o);
2200
+ if (oi !== null && oi === outVal) return [{ ...subst }];
2201
+ if (numEqualTerm(g.o, Number(outVal))) return [{ ...subst }];
2202
+ return [];
2203
+ }
2204
+
2205
+ const oi = parseIntLiteral(g.o);
2206
+ if (oi !== null) {
2207
+ const inVal = -oi;
2208
+ const lit = makeNumericOutputLiteral(inVal, XSD_INTEGER_DT);
2209
+ if (g.s instanceof Var) {
2210
+ const s2 = { ...subst };
2211
+ s2[g.s.name] = lit;
2212
+ return [s2];
2213
+ }
2214
+ if (g.s instanceof Blank) return [{ ...subst }];
2215
+ const si2 = parseIntLiteral(g.s);
2216
+ if (si2 !== null && si2 === inVal) return [{ ...subst }];
2217
+ if (numEqualTerm(g.s, Number(inVal))) return [{ ...subst }];
2218
+ return [];
2219
+ }
2220
+
2171
2221
  const neg = (x) => -x;
2172
2222
  return evalUnaryMathRel(g, subst, neg, neg);
2173
2223
  }
@@ -2226,6 +2276,25 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
2226
2276
  // Schema: $s+ math:rounded $o-
2227
2277
  // Note: spec says $o is xsd:integer, but we also accept any numeric $o that equals the rounded value.
2228
2278
  if (pv === MATH_NS + 'rounded') {
2279
+ const ai = parseIntLiteral(g.s);
2280
+ if (ai !== null) {
2281
+ const lit = makeNumericOutputLiteral(ai, XSD_INTEGER_DT);
2282
+
2283
+ if (g.o instanceof Var) {
2284
+ const s2 = { ...subst };
2285
+ s2[g.o.name] = lit;
2286
+ return [s2];
2287
+ }
2288
+ if (g.o instanceof Blank) return [{ ...subst }];
2289
+
2290
+ const oi = parseIntLiteral(g.o);
2291
+ if (oi !== null && oi === ai) return [{ ...subst }];
2292
+ if (numEqualTerm(g.o, Number(ai))) return [{ ...subst }];
2293
+
2294
+ const s2 = unifyTerm(g.o, lit, subst);
2295
+ return s2 !== null ? [s2] : [];
2296
+ }
2297
+
2229
2298
  const a = parseNum(g.s);
2230
2299
  if (a === null) return [];
2231
2300
  if (Number.isNaN(a)) return [];
package/lib/engine.js CHANGED
@@ -2651,6 +2651,18 @@ function __printTriggeredFuse(rule, prefixes, subst /* optional */, extraNote /*
2651
2651
  }
2652
2652
  }
2653
2653
 
2654
+ function __exitReasoning(code, message /* optional */) {
2655
+ if (typeof process !== 'undefined' && process && typeof process.exit === 'function') {
2656
+ process.exit(code);
2657
+ return;
2658
+ }
2659
+
2660
+ const err = new Error(message || `Process exited with code ${code}`);
2661
+ err.__eyelingExit = true;
2662
+ err.code = code;
2663
+ throw err;
2664
+ }
2665
+
2654
2666
  function forwardChain(facts, forwardRules, backRules, onDerived /* optional */, opts = {}) {
2655
2667
  enterReasoningRun();
2656
2668
  try {
@@ -2790,7 +2802,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
2790
2802
  // Allow dynamic fuses: ... => ?X. where ?X becomes false
2791
2803
  if (dynTerm instanceof Literal && dynTerm.value === 'false') {
2792
2804
  __printTriggeredFuse(r, opts && opts.prefixes, s, 'Dynamic head resolved to false.');
2793
- process.exit(2);
2805
+ __exitReasoning(2, 'Inference fuse triggered.');
2794
2806
  }
2795
2807
 
2796
2808
  const dynTriples = __graphTriplesOrTrue(dynTerm);
@@ -2818,10 +2830,10 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
2818
2830
  const objIsGraph = obj instanceof GraphTerm;
2819
2831
  const subjIsTrue = subj instanceof Literal && subj.value === 'true';
2820
2832
  const objIsTrue = obj instanceof Literal && obj.value === 'true';
2833
+ const objIsFalse = obj instanceof Literal && obj.value === 'false';
2821
2834
 
2822
2835
  const isFwRuleTriple =
2823
- isLogImplies(instantiated.p) &&
2824
- ((subjIsGraph && objIsGraph) || (subjIsTrue && objIsGraph) || (subjIsGraph && objIsTrue));
2836
+ isLogImplies(instantiated.p) && (subjIsGraph || subjIsTrue) && (objIsGraph || objIsTrue || objIsFalse);
2825
2837
 
2826
2838
  const isBwRuleTriple =
2827
2839
  isLogImpliedBy(instantiated.p) &&
@@ -2836,47 +2848,39 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
2836
2848
  changedHere = true;
2837
2849
  }
2838
2850
 
2839
- // Promote rule-producing triples to live rules, treating literal true as {}.
2840
- const left = __graphTriplesOrTrue(subj);
2841
- const right = __graphTriplesOrTrue(obj);
2842
-
2843
- if (left !== null && right !== null) {
2844
- if (isFwRuleTriple) {
2845
- const [premise, conclusion] = liftBlankRuleVars(left, right);
2846
- const headBlankLabels = collectBlankLabelsInTriples(conclusion);
2847
- const newRule = new Rule(premise, conclusion, true, false, headBlankLabels);
2848
- __prepareForwardRule(newRule);
2849
-
2850
- const key = __ruleKey(
2851
- newRule.isForward,
2852
- newRule.isFuse,
2853
- newRule.premise,
2854
- newRule.conclusion,
2855
- newRule.__dynamicConclusionTerm || null,
2856
- );
2857
- if (!forwardRules.__ruleKeySet.has(key)) {
2858
- forwardRules.__ruleKeySet.add(key);
2859
- forwardRules.push(newRule);
2860
- rulesChanged = true;
2861
- }
2862
- } else if (isBwRuleTriple) {
2863
- const [premise, conclusion] = liftBlankRuleVars(right, left);
2864
- const headBlankLabels = collectBlankLabelsInTriples(conclusion);
2865
- const newRule = new Rule(premise, conclusion, false, false, headBlankLabels);
2866
-
2867
- const key = __ruleKey(
2868
- newRule.isForward,
2869
- newRule.isFuse,
2870
- newRule.premise,
2871
- newRule.conclusion,
2872
- newRule.__dynamicConclusionTerm || null,
2873
- );
2874
- if (!backRules.__ruleKeySet.has(key)) {
2875
- backRules.__ruleKeySet.add(key);
2876
- backRules.push(newRule);
2877
- indexBackRule(backRules, newRule);
2878
- rulesChanged = true;
2879
- }
2851
+ // Promote rule-producing triples to live rules, treating literal true as {}
2852
+ // and literal false as a fuse head.
2853
+ if (isFwRuleTriple) {
2854
+ const newRule = __makeRuleFromTerms(subj, obj, true);
2855
+ __prepareForwardRule(newRule);
2856
+
2857
+ const key = __ruleKey(
2858
+ newRule.isForward,
2859
+ newRule.isFuse,
2860
+ newRule.premise,
2861
+ newRule.conclusion,
2862
+ newRule.__dynamicConclusionTerm || null,
2863
+ );
2864
+ if (!forwardRules.__ruleKeySet.has(key)) {
2865
+ forwardRules.__ruleKeySet.add(key);
2866
+ forwardRules.push(newRule);
2867
+ rulesChanged = true;
2868
+ }
2869
+ } else if (isBwRuleTriple) {
2870
+ const newRule = __makeRuleFromTerms(subj, obj, false);
2871
+
2872
+ const key = __ruleKey(
2873
+ newRule.isForward,
2874
+ newRule.isFuse,
2875
+ newRule.premise,
2876
+ newRule.conclusion,
2877
+ newRule.__dynamicConclusionTerm || null,
2878
+ );
2879
+ if (!backRules.__ruleKeySet.has(key)) {
2880
+ backRules.__ruleKeySet.add(key);
2881
+ backRules.push(newRule);
2882
+ indexBackRule(backRules, newRule);
2883
+ rulesChanged = true;
2880
2884
  }
2881
2885
  }
2882
2886
 
@@ -2959,7 +2963,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
2959
2963
  // Inference fuse
2960
2964
  if (r.isFuse && sols.length) {
2961
2965
  __printTriggeredFuse(r, opts && opts.prefixes, sols[0]);
2962
- process.exit(2);
2966
+ __exitReasoning(2, 'Inference fuse triggered.');
2963
2967
  }
2964
2968
 
2965
2969
  for (const s of sols) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.19.3",
3
+ "version": "1.19.5",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -1498,6 +1498,33 @@ _:x :hates { _:foo :making :mess }.
1498
1498
  ],
1499
1499
  },
1500
1500
 
1501
+ {
1502
+ name: '59c regression: integer-safe math absoluteValue, negation, and rounded preserve large integers',
1503
+ opt: { proofComments: false },
1504
+ input: `@prefix : <http://example.org/> .
1505
+ @prefix math: <http://www.w3.org/2000/10/swap/math#> .
1506
+ @base <http://example.org/> .
1507
+
1508
+ { 9999999999999999 math:absoluteValue ?X. ?X math:notEqualTo 9999999999999999. } => { :result :has :fail-abs. }.
1509
+ { 9999999999999999 math:negation ?X. ?X math:negation ?Y. ?Y math:notEqualTo 9999999999999999. } => { :result :has :fail-neg. }.
1510
+ { 9999999999999999 math:rounded ?X. ?X math:notEqualTo 9999999999999999. } => { :result :has :fail-round. }.
1511
+
1512
+ { } => {
1513
+ :test :contains :fail-abs, :fail-neg, :fail-round.
1514
+ }.
1515
+ `,
1516
+ expect: [
1517
+ /:(?:test)\s+:(?:contains)\s+:(?:fail-abs)\s*\./,
1518
+ /:(?:test)\s+:(?:contains)\s+:(?:fail-neg)\s*\./,
1519
+ /:(?:test)\s+:(?:contains)\s+:(?:fail-round)\s*\./,
1520
+ ],
1521
+ notExpect: [
1522
+ /:(?:result)\s+:(?:has)\s+:(?:fail-abs)\s*\./,
1523
+ /:(?:result)\s+:(?:has)\s+:(?:fail-neg)\s*\./,
1524
+ /:(?:result)\s+:(?:has)\s+:(?:fail-round)\s*\./,
1525
+ ],
1526
+ },
1527
+
1501
1528
  {
1502
1529
  name: '60 regression: log:includes must match quoted triples with variable predicates',
1503
1530
  opt: { proofComments: false },
@@ -1,5 +0,0 @@
1
- 'use strict';
2
-
3
- module.exports = {
4
- 'http://example.org/test#bad-return': () => ({ nope: true }),
5
- };