eyeling 1.13.7 → 1.14.1

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
@@ -1516,6 +1516,47 @@ A tiny `sprintf` subset:
1516
1516
  - Any other specifier (`%d`, `%f`, …) causes the builtin to fail.
1517
1517
  - Missing arguments are treated as empty strings.
1518
1518
 
1519
+ ### Length and character utilities (Eyeling extensions)
1520
+
1521
+ Eyeling also implements a few **non-standard** `string:` helpers that are handy for string-based algorithms. These are **not** part of the SWAP builtin set, so treat them as Eyeling extensions.
1522
+
1523
+ #### `string:length`
1524
+
1525
+ **Shape:** `s string:length n`
1526
+
1527
+ Casts `s` to a string and returns its length as an integer literal token.
1528
+
1529
+ #### `string:charAt`
1530
+
1531
+ **Shape:** `( s i ) string:charAt ch`
1532
+
1533
+ - `i` is a numeric term, truncated to an integer.
1534
+ - Indexing is **0-based** (like JavaScript).
1535
+ - If `i` is out of range, `ch` is the empty string `""`.
1536
+
1537
+ #### `string:setCharAt`
1538
+
1539
+ **Shape:** `( s i ch ) string:setCharAt out`
1540
+
1541
+ Returns a copy of `s` with the character at index `i` (0-based) replaced by:
1542
+
1543
+ - the **first character** of `ch` if `ch` is non-empty, otherwise
1544
+ - the empty string.
1545
+
1546
+ If `i` is out of range, `out` is the original string.
1547
+
1548
+ #### `string:hammingDistance`
1549
+
1550
+ **Shape:** `( a b ) string:hammingDistance d`
1551
+
1552
+ Returns the number of differing positions between `a` and `b`. Fails if the two strings have different lengths.
1553
+
1554
+ #### `string:mutateSelectBest`
1555
+
1556
+ **Shape:** `( samples current target mutProb seedState ) string:mutateSelectBest ( best bestScore seedState2 )`
1557
+
1558
+ Generates `samples` mutated variants of `current` (mutating each character with probability `mutProb` percent), scores each candidate against `target` (Hamming distance), and returns the best candidate (lowest score). RNG is a deterministic 31-bit LCG, threaded via `seedState` → `seedState2`, so runs are reproducible.
1559
+
1519
1560
  ### Containment and prefix/suffix tests
1520
1561
 
1521
1562
  - `string:contains`
@@ -0,0 +1,177 @@
1
+ # ==========================================================================================
2
+ # Genetic algorithm demo
3
+ #
4
+ # Genetic algorithm (GA) in a nutshell:
5
+ # - Start with a random candidate string of the same length as the target.
6
+ # - Repeatedly create a batch of “children” by copying the current best string and randomly
7
+ # mutating each character with a small probability.
8
+ # - Score each child by how many positions differ from the target (Hamming distance).
9
+ # - Keep the best-scoring child as the new current string and iterate until the score is 0.
10
+ # This is a simple hill-climbing GA (selection + mutation, no crossover), using a seeded RNG
11
+ # so runs are reproducible.
12
+ #
13
+ # The GA is still expressed as backward rules (<=) for the outer evolution loop,
14
+ # but the expensive inner loop ("take N mutated samples and keep the best") is
15
+ # handled by a single string builtin to avoid huge proof trees and OOM.
16
+ #
17
+ # ==========================================================================================
18
+
19
+ @prefix : <urn:ga:>.
20
+ @prefix log: <http://www.w3.org/2000/10/swap/log#>.
21
+ @prefix math: <http://www.w3.org/2000/10/swap/math#>.
22
+ @prefix list: <http://www.w3.org/2000/10/swap/list#>.
23
+ @prefix string: <http://www.w3.org/2000/10/swap/string#>.
24
+
25
+ :cfg
26
+ :mutationProbability 5;
27
+ :samples 80;
28
+ :seed 100;
29
+
30
+ # Debug knobs:
31
+ :traceEvery 1; # 0 disables; 1 traces every generation
32
+ :maxGenerations 0; # 0 means unlimited
33
+
34
+ :alphabet (" " "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M"
35
+ "N" "O" "P" "Q" "R" "S" "T" "U" "V" "W" "X" "Y" "Z").
36
+
37
+ # ------------------------
38
+ # Small arithmetic helpers
39
+ # ------------------------
40
+
41
+ { ( ?N 1 ) :dec ?N1 } <= { ( ?N 1 ) math:difference ?N1 }.
42
+ { ( ?N 1 ) :inc ?N1 } <= { ( ?N 1 ) math:sum ?N1 }.
43
+
44
+ # ----------------------------------------------
45
+ # Seeded RNG (LCG) and JS Math.round(rnd() * N)
46
+ # (Used only for the initial random chromosome.)
47
+ # ----------------------------------------------
48
+
49
+ { ?S0 :lcgNext ?S1 } <= {
50
+ ( ?S0 1103515245 ) math:product ?P .
51
+ ( ?P 12345 ) math:sum ?T .
52
+ ( ?T 2147483648 ) math:remainder ?S1
53
+ }.
54
+
55
+ { ( ?S0 ?N ) :randRound ( ?R ?S1 ) } <= {
56
+ ?S0 :lcgNext ?S1 .
57
+ ( ?S1 ?N ) math:product ?A .
58
+ ( ?A ?A ) math:sum ?TwoA .
59
+ ( ?TwoA 2147483648 ) math:sum ?Num .
60
+ ( 2147483648 2 ) math:product ?Den .
61
+ ( ?Num ?Den ) math:integerQuotient ?R
62
+ }.
63
+
64
+ { ?S0 :randomAlpha ( ?Ch ?S1 ) } <= {
65
+ :cfg :alphabet ?Alphabet .
66
+ ( ?S0 26 ) :randRound ( ?Idx ?S1 ) .
67
+ ( ?Alphabet ?Idx ) list:memberAt ?Ch
68
+ }.
69
+
70
+ { ( 0 ?S0 ) :randomText ( "" ?S0 ) } <= true.
71
+
72
+ { ( ?Len ?S0 ) :randomText ( ?Out ?S2 ) } <= {
73
+ ?Len math:greaterThan 0 .
74
+ ( ?Len 1 ) :dec ?Len1 .
75
+ ?S0 :randomAlpha ( ?Ch ?S1 ) .
76
+ ( ?Len1 ?S1 ) :randomText ( ?Rest ?S2 ) .
77
+ ( "%s%s" ?Ch ?Rest ) string:format ?Out
78
+ }.
79
+
80
+ # -------------------------
81
+ # tracing helper (optional)
82
+ # -------------------------
83
+
84
+ { ( 0 ?Gen ?Score ?Value ) :traceMaybe true } <= true.
85
+
86
+ { ( ?Every ?Gen ?Score ?Value ) :traceMaybe true } <= {
87
+ ?Every math:greaterThan 0 .
88
+ ( ?Gen ?Every ) math:remainder ?R .
89
+ ?R math:equalTo 0 .
90
+ ?Gen log:trace ( ?Score ?Value )
91
+ }.
92
+
93
+ # --------------
94
+ # evolution loop
95
+ # --------------
96
+
97
+ # Stop when score == 0
98
+ { ( ?Current 0 ?Target ?MutProb ?Samples ?Gen ?S0 ?MaxGen ?TraceEvery )
99
+ :evolve ( ?Current ?Gen ?S0 ) } <= true.
100
+
101
+ # Stop when generation cap is reached (only if MaxGen > 0)
102
+ { ( ?Current ?Score ?Target ?MutProb ?Samples ?Gen ?S0 ?MaxGen ?TraceEvery )
103
+ :evolve ( ?Current ?Gen ?S0 ) } <= {
104
+ ?Score math:notEqualTo 0 .
105
+ ?MaxGen math:greaterThan 0 .
106
+ ?Gen math:equalTo ?MaxGen
107
+ }.
108
+
109
+ # Unlimited run (MaxGen == 0): keep evolving
110
+ { ( ?Current ?Score ?Target ?MutProb ?Samples ?Gen ?S0 0 ?TraceEvery )
111
+ :evolve ( ?Final ?FinalGen ?S2 ) } <= {
112
+ ?Score math:notEqualTo 0 .
113
+ ( ?Samples ?Current ?Target ?MutProb ?S0 )
114
+ string:mutateSelectBest
115
+ ( ?Best ?BestScore ?S1 ) .
116
+ ( ?Gen 1 ) :inc ?Gen1 .
117
+ ( ?TraceEvery ?Gen1 ?BestScore ?Best ) :traceMaybe true .
118
+ ( ?Best ?BestScore ?Target ?MutProb ?Samples ?Gen1 ?S1 0 ?TraceEvery )
119
+ :evolve ( ?Final ?FinalGen ?S2 )
120
+ }.
121
+
122
+ # Bounded run (MaxGen > 0): keep evolving while Gen < MaxGen
123
+ { ( ?Current ?Score ?Target ?MutProb ?Samples ?Gen ?S0 ?MaxGen ?TraceEvery )
124
+ :evolve ( ?Final ?FinalGen ?S2 ) } <= {
125
+ ?Score math:notEqualTo 0 .
126
+ ?MaxGen math:greaterThan 0 .
127
+ ?Gen math:lessThan ?MaxGen .
128
+ ( ?Samples ?Current ?Target ?MutProb ?S0 )
129
+ string:mutateSelectBest
130
+ ( ?Best ?BestScore ?S1 ) .
131
+ ( ?Gen 1 ) :inc ?Gen1 .
132
+ ( ?TraceEvery ?Gen1 ?BestScore ?Best ) :traceMaybe true .
133
+ ( ?Best ?BestScore ?Target ?MutProb ?Samples ?Gen1 ?S1 ?MaxGen ?TraceEvery )
134
+ :evolve ( ?Final ?FinalGen ?S2 )
135
+ }.
136
+
137
+ # ----------------------------------
138
+ # Public solver wrapper (string API)
139
+ # ----------------------------------
140
+
141
+ { ?TargetString :solveResult ( ?Gen 0 ?Value ?Seed ) } <= {
142
+ :cfg :mutationProbability ?MutProb;
143
+ :samples ?Samples;
144
+ :seed ?Seed;
145
+ :traceEvery ?TraceEvery;
146
+ :maxGenerations ?MaxGen.
147
+
148
+ ?TargetString string:length ?Len .
149
+
150
+ ( ?Len ?Seed ) :randomText ( ?Current0 ?S1 ) .
151
+
152
+ # initial score using string:hammingDistance
153
+ ( ?Current0 ?TargetString ) string:hammingDistance ?Score0 .
154
+
155
+ ( ?Current0 ?Score0 ?TargetString ?MutProb ?Samples 0 ?S1 ?MaxGen ?TraceEvery )
156
+ :evolve
157
+ ( ?Value ?Gen ?S2 ) .
158
+
159
+ # Succeeds only when solved:
160
+ ( ?Value ?TargetString ) string:hammingDistance 0 .
161
+ ?Value log:equalTo ?TargetString .
162
+ ?S2 log:equalTo ?_FinalSeedState
163
+ }.
164
+
165
+ { ?TargetString :solved true } <= {
166
+ ?TargetString :solveResult ( ?Gen 0 ?TargetString ?Seed )
167
+ }.
168
+
169
+ # --------------------------------------------------------
170
+ # Requested output: solve('METHINKS IT IS LIKE A WEASEL').
171
+ # --------------------------------------------------------
172
+
173
+ { "METHINKS IT IS LIKE A WEASEL" :solved true .
174
+ ( "solve('%s').\n" "METHINKS IT IS LIKE A WEASEL" ) string:format ?Line
175
+ }
176
+ log:query
177
+ { 1 log:outputString ?Line }.
@@ -0,0 +1,3 @@
1
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
2
+
3
+ 1 log:outputString "solve('METHINKS IT IS LIKE A WEASEL').\n" .
@@ -5,6 +5,7 @@
5
5
  @prefix list: <http://www.w3.org/2000/10/swap/list#> .
6
6
  @prefix log: <http://www.w3.org/2000/10/swap/log#> .
7
7
  @prefix string: <http://www.w3.org/2000/10/swap/string#> .
8
+ @prefix ega: <urn:eyeling:ga:> .
8
9
  @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
9
10
  @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
10
11
  @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@@ -333,3 +334,17 @@ string:scrape a ex:Builtin ; ex:kind ex:Function ;
333
334
  string:format a ex:Builtin ; ex:kind ex:Function ;
334
335
  rdfs:comment "Simple formatter: (fmt arg1 ... argN) -> output string. Only %s and %% are supported; other specifiers fail." .
335
336
 
337
+ string:length a ex:Builtin ; ex:kind ex:Function ;
338
+ rdfs:comment "Function: binds/unifies object with the character length of the subject after string-casting. Output is an integer literal token." .
339
+
340
+ string:charAt a ex:Builtin ; ex:kind ex:Function ;
341
+ rdfs:comment "Function: subject is a 2-item list (string idx). Binds/unifies object with the 1-character string at 0-based idx; out-of-range yields the empty string." .
342
+
343
+ string:setCharAt a ex:Builtin ; ex:kind ex:Function ;
344
+ rdfs:comment "Function: subject is a 3-item list (string idx ch). Returns a copy of the string with position idx (0-based) replaced by the first character of ch; out-of-range returns the original string." .
345
+
346
+ string:hammingDistance a ex:Builtin ; ex:kind ex:Function ;
347
+ rdfs:comment "Function: subject is a 2-item list (a b). Binds/unifies object with the number of differing positions (Hamming distance). Fails if lengths differ." .
348
+
349
+ string:mutateSelectBest a ex:Builtin ; ex:kind ex:Function ;
350
+ rdfs:comment "Shape: (samples current target mutProb seedState) string:mutateSelectBest (best bestScore seedState2). Generates samples mutated variants of current (mutating each character with probability mutProb percent), scores each candidate against target (Hamming distance), and returns the best candidate (lowest score). RNG is a deterministic 31-bit LCG, threaded via seedState → seedState2, so runs are reproducible." .
package/eyeling.js CHANGED
@@ -3463,6 +3463,176 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3463
3463
  return sStr.startsWith(oStr) ? [{ ...subst }] : [];
3464
3464
  }
3465
3465
 
3466
+ // -----------------------------------------------------------------
3467
+ // 4.6.1 extended string: builtins (Eyeling extensions)
3468
+ // -----------------------------------------------------------------
3469
+
3470
+ // string:length
3471
+ // Schema: $s+ string:length $o?
3472
+ // $o is an integer literal (untyped numeric token).
3473
+ if (pv === STRING_NS + 'length') {
3474
+ const sStr = termToJsString(g.s);
3475
+ if (sStr === null) return [];
3476
+ const lit = internLiteral(String(sStr.length));
3477
+ if (g.o instanceof Var) {
3478
+ const s2 = { ...subst };
3479
+ s2[g.o.name] = lit;
3480
+ return [s2];
3481
+ }
3482
+ const s2 = unifyTerm(g.o, lit, subst);
3483
+ return s2 !== null ? [s2] : [];
3484
+ }
3485
+
3486
+ // string:charAt
3487
+ // Schema: ( $str $idx ) string:charAt $ch
3488
+ // $ch is a (possibly empty) string literal.
3489
+ if (pv === STRING_NS + 'charAt') {
3490
+ if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3491
+ const sStr = termToJsString(g.s.elems[0]);
3492
+ const idxNum = parseNum(g.s.elems[1]);
3493
+ if (sStr === null || idxNum === null) return [];
3494
+ const idx = Math.trunc(idxNum);
3495
+ const ch = idx < 0 || idx >= sStr.length ? '' : sStr.charAt(idx);
3496
+ const lit = makeStringLiteral(ch);
3497
+ if (g.o instanceof Var) {
3498
+ const s2 = { ...subst };
3499
+ s2[g.o.name] = lit;
3500
+ return [s2];
3501
+ }
3502
+ const s2 = unifyTerm(g.o, lit, subst);
3503
+ return s2 !== null ? [s2] : [];
3504
+ }
3505
+
3506
+ // string:setCharAt
3507
+ // Schema: ( $str $idx $ch ) string:setCharAt $out
3508
+ // If idx out of range, returns the original string.
3509
+ if (pv === STRING_NS + 'setCharAt') {
3510
+ if (!(g.s instanceof ListTerm) || g.s.elems.length !== 3) return [];
3511
+ const sStr = termToJsString(g.s.elems[0]);
3512
+ const idxNum = parseNum(g.s.elems[1]);
3513
+ const chStr = termToJsString(g.s.elems[2]);
3514
+ if (sStr === null || idxNum === null || chStr === null) return [];
3515
+ const idx = Math.trunc(idxNum);
3516
+ const rep = chStr.length ? chStr[0] : '';
3517
+ let out = sStr;
3518
+ if (idx >= 0 && idx < sStr.length) out = sStr.slice(0, idx) + rep + sStr.slice(idx + 1);
3519
+ const lit = makeStringLiteral(out);
3520
+ if (g.o instanceof Var) {
3521
+ const s2 = { ...subst };
3522
+ s2[g.o.name] = lit;
3523
+ return [s2];
3524
+ }
3525
+ const s2 = unifyTerm(g.o, lit, subst);
3526
+ return s2 !== null ? [s2] : [];
3527
+ }
3528
+
3529
+ // string:hammingDistance
3530
+ // Schema: ( $a $b ) string:hammingDistance $d
3531
+ // Fails if strings have different length.
3532
+ if (pv === STRING_NS + 'hammingDistance') {
3533
+ if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3534
+ const a = termToJsString(g.s.elems[0]);
3535
+ const b = termToJsString(g.s.elems[1]);
3536
+ if (a === null || b === null) return [];
3537
+ if (a.length !== b.length) return [];
3538
+ let diffs = 0;
3539
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) diffs++;
3540
+ const lit = internLiteral(String(diffs));
3541
+ if (g.o instanceof Var) {
3542
+ const s2 = { ...subst };
3543
+ s2[g.o.name] = lit;
3544
+ return [s2];
3545
+ }
3546
+ const s2 = unifyTerm(g.o, lit, subst);
3547
+ return s2 !== null ? [s2] : [];
3548
+ }
3549
+
3550
+ // string:mutateSelectBest
3551
+ // Schema:
3552
+ // ( $samples $current $target $mutProb $seedState )
3553
+ // string:mutateSelectBest
3554
+ // ( $best $bestScore $seedState2 )
3555
+ //
3556
+ // Deterministic 31-bit LCG (same as the GA demo):
3557
+ // state = (1103515245 * state + 12345) % 2^31
3558
+ // and randRound(N) = Math.round((state/2^31) * N) with state advanced once per draw.
3559
+ //
3560
+ // This builtin exists to keep GA-style "many samples × many characters" loops out of
3561
+ // the proof search (dramatically reducing memory use).
3562
+ if (pv === STRING_NS + 'mutateSelectBest') {
3563
+ if (!(g.s instanceof ListTerm) || g.s.elems.length !== 5) return [];
3564
+
3565
+ const samplesNum = parseNum(g.s.elems[0]);
3566
+ const current = termToJsXsdStringNoLang(g.s.elems[1]);
3567
+ const target = termToJsXsdStringNoLang(g.s.elems[2]);
3568
+ const mutProbNum = parseNum(g.s.elems[3]);
3569
+ const seedNum = parseNum(g.s.elems[4]);
3570
+
3571
+ if (samplesNum === null || current === null || target === null || mutProbNum === null || seedNum === null)
3572
+ return [];
3573
+ if (current.length !== target.length) return [];
3574
+
3575
+ const samples = Math.max(1, Math.trunc(samplesNum));
3576
+ const mutProb = Math.max(0, Math.trunc(mutProbNum));
3577
+ const seed0 = Math.trunc(seedNum);
3578
+
3579
+ let state = BigInt(seed0);
3580
+ const MOD = 2147483648n; // 2^31
3581
+ const A = 1103515245n;
3582
+ const C = 12345n;
3583
+
3584
+ function rnd() {
3585
+ state = (A * state + C) % MOD;
3586
+ return Number(state) / 2147483648;
3587
+ }
3588
+
3589
+ function randRound(N) {
3590
+ return Math.round(rnd() * N);
3591
+ }
3592
+
3593
+ function randomAlpha() {
3594
+ const p = randRound(26);
3595
+ return p === 0 ? ' ' : String.fromCharCode(64 + p);
3596
+ }
3597
+
3598
+ function mutate(str) {
3599
+ let out = '';
3600
+ for (let i = 0; i < str.length; i++) {
3601
+ const p = randRound(100);
3602
+ if (p > mutProb) out += str[i];
3603
+ else out += randomAlpha();
3604
+ }
3605
+ return out;
3606
+ }
3607
+
3608
+ function score(str) {
3609
+ let diffs = 0;
3610
+ for (let i = 0; i < target.length; i++) if (str[i] !== target[i]) diffs++;
3611
+ return diffs;
3612
+ }
3613
+
3614
+ let best = '';
3615
+ let bestScore = Infinity;
3616
+
3617
+ for (let i = 0; i < samples; i++) {
3618
+ const cand = mutate(current);
3619
+ const candScore = score(cand);
3620
+ if (candScore < bestScore) {
3621
+ best = cand;
3622
+ bestScore = candScore;
3623
+ }
3624
+ }
3625
+
3626
+ const outList = new ListTerm([
3627
+ makeStringLiteral(best),
3628
+ internLiteral(String(bestScore)),
3629
+ internLiteral(state.toString()),
3630
+ ]);
3631
+
3632
+ const s2 = unifyTerm(g.o, outList, subst);
3633
+ return s2 !== null ? [s2] : [];
3634
+ }
3635
+
3466
3636
  // Unknown builtin
3467
3637
  return [];
3468
3638
  }
package/lib/builtins.js CHANGED
@@ -3451,6 +3451,173 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3451
3451
  return sStr.startsWith(oStr) ? [{ ...subst }] : [];
3452
3452
  }
3453
3453
 
3454
+ // -----------------------------------------------------------------
3455
+ // 4.6.1 extended string: builtins (Eyeling extensions)
3456
+ // -----------------------------------------------------------------
3457
+
3458
+ // string:length
3459
+ // Schema: $s+ string:length $o?
3460
+ // $o is an integer literal (untyped numeric token).
3461
+ if (pv === STRING_NS + 'length') {
3462
+ const sStr = termToJsString(g.s);
3463
+ if (sStr === null) return [];
3464
+ const lit = internLiteral(String(sStr.length));
3465
+ if (g.o instanceof Var) {
3466
+ const s2 = { ...subst };
3467
+ s2[g.o.name] = lit;
3468
+ return [s2];
3469
+ }
3470
+ const s2 = unifyTerm(g.o, lit, subst);
3471
+ return s2 !== null ? [s2] : [];
3472
+ }
3473
+
3474
+ // string:charAt
3475
+ // Schema: ( $str $idx ) string:charAt $ch
3476
+ // $ch is a (possibly empty) string literal.
3477
+ if (pv === STRING_NS + 'charAt') {
3478
+ if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3479
+ const sStr = termToJsString(g.s.elems[0]);
3480
+ const idxNum = parseNum(g.s.elems[1]);
3481
+ if (sStr === null || idxNum === null) return [];
3482
+ const idx = Math.trunc(idxNum);
3483
+ const ch = idx < 0 || idx >= sStr.length ? '' : sStr.charAt(idx);
3484
+ const lit = makeStringLiteral(ch);
3485
+ if (g.o instanceof Var) {
3486
+ const s2 = { ...subst };
3487
+ s2[g.o.name] = lit;
3488
+ return [s2];
3489
+ }
3490
+ const s2 = unifyTerm(g.o, lit, subst);
3491
+ return s2 !== null ? [s2] : [];
3492
+ }
3493
+
3494
+ // string:setCharAt
3495
+ // Schema: ( $str $idx $ch ) string:setCharAt $out
3496
+ // If idx out of range, returns the original string.
3497
+ if (pv === STRING_NS + 'setCharAt') {
3498
+ if (!(g.s instanceof ListTerm) || g.s.elems.length !== 3) return [];
3499
+ const sStr = termToJsString(g.s.elems[0]);
3500
+ const idxNum = parseNum(g.s.elems[1]);
3501
+ const chStr = termToJsString(g.s.elems[2]);
3502
+ if (sStr === null || idxNum === null || chStr === null) return [];
3503
+ const idx = Math.trunc(idxNum);
3504
+ const rep = chStr.length ? chStr[0] : '';
3505
+ let out = sStr;
3506
+ if (idx >= 0 && idx < sStr.length) out = sStr.slice(0, idx) + rep + sStr.slice(idx + 1);
3507
+ const lit = makeStringLiteral(out);
3508
+ if (g.o instanceof Var) {
3509
+ const s2 = { ...subst };
3510
+ s2[g.o.name] = lit;
3511
+ return [s2];
3512
+ }
3513
+ const s2 = unifyTerm(g.o, lit, subst);
3514
+ return s2 !== null ? [s2] : [];
3515
+ }
3516
+
3517
+ // string:hammingDistance
3518
+ // Schema: ( $a $b ) string:hammingDistance $d
3519
+ // Fails if strings have different length.
3520
+ if (pv === STRING_NS + 'hammingDistance') {
3521
+ if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3522
+ const a = termToJsString(g.s.elems[0]);
3523
+ const b = termToJsString(g.s.elems[1]);
3524
+ if (a === null || b === null) return [];
3525
+ if (a.length !== b.length) return [];
3526
+ let diffs = 0;
3527
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) diffs++;
3528
+ const lit = internLiteral(String(diffs));
3529
+ if (g.o instanceof Var) {
3530
+ const s2 = { ...subst };
3531
+ s2[g.o.name] = lit;
3532
+ return [s2];
3533
+ }
3534
+ const s2 = unifyTerm(g.o, lit, subst);
3535
+ return s2 !== null ? [s2] : [];
3536
+ }
3537
+
3538
+ // string:mutateSelectBest
3539
+ // Schema:
3540
+ // ( $samples $current $target $mutProb $seedState )
3541
+ // string:mutateSelectBest
3542
+ // ( $best $bestScore $seedState2 )
3543
+ //
3544
+ // Deterministic 31-bit LCG (same as the GA demo):
3545
+ // state = (1103515245 * state + 12345) % 2^31
3546
+ // and randRound(N) = Math.round((state/2^31) * N) with state advanced once per draw.
3547
+ if (pv === STRING_NS + 'mutateSelectBest') {
3548
+ if (!(g.s instanceof ListTerm) || g.s.elems.length !== 5) return [];
3549
+
3550
+ const samplesNum = parseNum(g.s.elems[0]);
3551
+ const current = termToJsXsdStringNoLang(g.s.elems[1]);
3552
+ const target = termToJsXsdStringNoLang(g.s.elems[2]);
3553
+ const mutProbNum = parseNum(g.s.elems[3]);
3554
+ const seedNum = parseNum(g.s.elems[4]);
3555
+
3556
+ if (samplesNum === null || current === null || target === null || mutProbNum === null || seedNum === null)
3557
+ return [];
3558
+ if (current.length !== target.length) return [];
3559
+
3560
+ const samples = Math.max(1, Math.trunc(samplesNum));
3561
+ const mutProb = Math.max(0, Math.trunc(mutProbNum));
3562
+ const seed0 = Math.trunc(seedNum);
3563
+
3564
+ let state = BigInt(seed0);
3565
+ const MOD = 2147483648n; // 2^31
3566
+ const A = 1103515245n;
3567
+ const C = 12345n;
3568
+
3569
+ function rnd() {
3570
+ state = (A * state + C) % MOD;
3571
+ return Number(state) / 2147483648;
3572
+ }
3573
+
3574
+ function randRound(N) {
3575
+ return Math.round(rnd() * N);
3576
+ }
3577
+
3578
+ function randomAlpha() {
3579
+ const p = randRound(26);
3580
+ return p === 0 ? ' ' : String.fromCharCode(64 + p);
3581
+ }
3582
+
3583
+ function mutate(str) {
3584
+ let out = '';
3585
+ for (let i = 0; i < str.length; i++) {
3586
+ const p = randRound(100);
3587
+ if (p > mutProb) out += str[i];
3588
+ else out += randomAlpha();
3589
+ }
3590
+ return out;
3591
+ }
3592
+
3593
+ function score(str) {
3594
+ let diffs = 0;
3595
+ for (let i = 0; i < target.length; i++) if (str[i] !== target[i]) diffs++;
3596
+ return diffs;
3597
+ }
3598
+
3599
+ let best = '';
3600
+ let bestScore = Infinity;
3601
+
3602
+ for (let i = 0; i < samples; i++) {
3603
+ const cand = mutate(current);
3604
+ const candScore = score(cand);
3605
+ if (candScore < bestScore) {
3606
+ best = cand;
3607
+ bestScore = candScore;
3608
+ }
3609
+ }
3610
+
3611
+ const outList = new ListTerm([
3612
+ makeStringLiteral(best),
3613
+ internLiteral(String(bestScore)),
3614
+ internLiteral(state.toString()),
3615
+ ]);
3616
+
3617
+ const s2 = unifyTerm(g.o, outList, subst);
3618
+ return s2 !== null ? [s2] : [];
3619
+ }
3620
+
3454
3621
  // Unknown builtin
3455
3622
  return [];
3456
3623
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.13.7",
3
+ "version": "1.14.1",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [