eyeling 1.13.7 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HANDBOOK.md +53 -0
- package/examples/genetic-algorithm.n3 +63 -0
- package/examples/output/genetic-algorithm.n3 +3 -0
- package/eyeling-builtins.ttl +17 -0
- package/eyeling.js +208 -0
- package/lib/builtins.js +208 -0
- package/package.json +1 -1
package/HANDBOOK.md
CHANGED
|
@@ -1516,6 +1516,41 @@ 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
|
+
|
|
1519
1554
|
### Containment and prefix/suffix tests
|
|
1520
1555
|
|
|
1521
1556
|
- `string:contains`
|
|
@@ -1941,6 +1976,24 @@ References:
|
|
|
1941
1976
|
|
|
1942
1977
|
If you are running untrusted inputs, consider `--super-restricted` to disable all builtins except implication.
|
|
1943
1978
|
|
|
1979
|
+
### Eyeling fast-path builtins
|
|
1980
|
+
|
|
1981
|
+
Eyeling ships one optional fast-path builtins builtin e.g. used by the genetic-algorithm example:
|
|
1982
|
+
|
|
1983
|
+
#### `urn:eyeling:ga:solveString`
|
|
1984
|
+
|
|
1985
|
+
**Shape:** `( target mutationProbability samples seed traceEvery maxGenerations ) urn:eyeling:ga:solveString ( generation score value seed )`
|
|
1986
|
+
|
|
1987
|
+
- `target` and `value` are `xsd:string` literals.
|
|
1988
|
+
- `mutationProbability` is interpreted as a percentage per character (0–100).
|
|
1989
|
+
- `traceEvery` controls debug tracing:
|
|
1990
|
+
- `0` disables tracing,
|
|
1991
|
+
- `1` traces every generation,
|
|
1992
|
+
- `N` traces every `N` generations.
|
|
1993
|
+
- `maxGenerations` is a safety cap:
|
|
1994
|
+
- `0` means unlimited,
|
|
1995
|
+
- otherwise the builtin stops once `generation >= maxGenerations` (even if not solved).
|
|
1996
|
+
|
|
1944
1997
|
### A.6 Skolemization and `log:skolem`
|
|
1945
1998
|
|
|
1946
1999
|
When forward rule heads contain blank nodes (existentials), Eyeling replaces them with generated Skolem IRIs so derived facts are ground.
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
# Uses Eyeling's builtin:
|
|
14
|
+
# urn:eyeling:ga:solveString
|
|
15
|
+
#
|
|
16
|
+
# Configure:
|
|
17
|
+
# :mutationProbability 5 # percent per character
|
|
18
|
+
# :samples 80
|
|
19
|
+
# :seed 100
|
|
20
|
+
# :traceEvery 0 # 0=off, 1=every generation, 100=every 100 generations
|
|
21
|
+
# :maxGenerations 0 # 0=unlimited
|
|
22
|
+
# ==========================================================================================
|
|
23
|
+
|
|
24
|
+
@prefix : <http://example.org/ga-solve#>.
|
|
25
|
+
@prefix ega: <urn:eyeling:ga:>.
|
|
26
|
+
@prefix log: <http://www.w3.org/2000/10/swap/log#>.
|
|
27
|
+
@prefix string: <http://www.w3.org/2000/10/swap/string#>.
|
|
28
|
+
|
|
29
|
+
:cfg
|
|
30
|
+
:mutationProbability 5;
|
|
31
|
+
:samples 80;
|
|
32
|
+
:seed 100;
|
|
33
|
+
:traceEvery 1;
|
|
34
|
+
:maxGenerations 0.
|
|
35
|
+
|
|
36
|
+
# solveResult(TargetString) = (generation score value seed)
|
|
37
|
+
{ ?TargetString :solveResult ( ?Gen ?Score ?Value ?Seed ) } <= {
|
|
38
|
+
:cfg :mutationProbability ?MutProb;
|
|
39
|
+
:samples ?Samples;
|
|
40
|
+
:seed ?Seed;
|
|
41
|
+
:traceEvery ?TraceEvery;
|
|
42
|
+
:maxGenerations ?MaxG.
|
|
43
|
+
|
|
44
|
+
( ?TargetString ?MutProb ?Samples ?Seed ?TraceEvery ?MaxG )
|
|
45
|
+
ega:solveString
|
|
46
|
+
( ?Gen ?Score ?Value ?SeedOut ) .
|
|
47
|
+
|
|
48
|
+
# By design, the 4th output is the (input) seed.
|
|
49
|
+
?SeedOut log:equalTo ?Seed
|
|
50
|
+
}.
|
|
51
|
+
|
|
52
|
+
# Boolean-ish API for querying success
|
|
53
|
+
{ ?TargetString :solved true } <= {
|
|
54
|
+
?TargetString :solveResult ( ?Gen 0 ?TargetString ?Seed )
|
|
55
|
+
}.
|
|
56
|
+
|
|
57
|
+
# --------------------------------------------------------
|
|
58
|
+
# Requested output: solve('METHINKS IT IS LIKE A WEASEL').
|
|
59
|
+
# --------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
{ "METHINKS IT IS LIKE A WEASEL" :solved true .
|
|
62
|
+
( "solve('%s').\n" "METHINKS IT IS LIKE A WEASEL" ) string:format ?Line
|
|
63
|
+
} => { 1 log:outputString ?Line }.
|
package/eyeling-builtins.ttl
CHANGED
|
@@ -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,19 @@ 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
|
+
# --- eyeling fast path builtins ------------------------------
|
|
350
|
+
|
|
351
|
+
ega:solveString a ex:Builtin ; ex:kind ex:Function ;
|
|
352
|
+
rdfs:comment "Eyeling extension: GA solver on strings. Subject list: (target mutationProbability samples seed traceEvery maxGenerations). Object list: (generation score value seed). traceEvery=0 disables tracing; maxGenerations=0 means unlimited." .
|
package/eyeling.js
CHANGED
|
@@ -3463,6 +3463,209 @@ 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
|
+
// -----------------------------------------------------------------
|
|
3551
|
+
// 4.6.2 urn:eyeling:ga: builtins (fast GA on *strings*)
|
|
3552
|
+
// -----------------------------------------------------------------
|
|
3553
|
+
|
|
3554
|
+
// urn:eyeling:ga:solveString
|
|
3555
|
+
// Schema: ( $target $mutProb $samples $seed $traceEvery $maxGenerations ) :solveString ( $gen $score $value $seed )
|
|
3556
|
+
// Side effect: if traceEvery > 0, prints: <gen> TRACE (<score> "<value>")
|
|
3557
|
+
if (pv === 'urn:eyeling:ga:solveString') {
|
|
3558
|
+
if (!(g.s instanceof ListTerm) || g.s.elems.length != 6) return [];
|
|
3559
|
+
|
|
3560
|
+
const target = termToJsXsdStringNoLang(g.s.elems[0]);
|
|
3561
|
+
const mutProbNum = parseNum(g.s.elems[1]);
|
|
3562
|
+
const samplesNum = parseNum(g.s.elems[2]);
|
|
3563
|
+
const seedNum = parseNum(g.s.elems[3]);
|
|
3564
|
+
const traceEveryNum = parseNum(g.s.elems[4]);
|
|
3565
|
+
const maxGenNum = parseNum(g.s.elems[5]);
|
|
3566
|
+
|
|
3567
|
+
if (target === null || mutProbNum === null || samplesNum === null || seedNum === null) return [];
|
|
3568
|
+
if (traceEveryNum === null || maxGenNum === null) return [];
|
|
3569
|
+
|
|
3570
|
+
const mutProb = Math.max(0, Math.trunc(mutProbNum));
|
|
3571
|
+
const samples = Math.max(1, Math.trunc(samplesNum));
|
|
3572
|
+
const seed0 = Math.trunc(seedNum);
|
|
3573
|
+
|
|
3574
|
+
const traceEvery = Math.max(0, Math.trunc(traceEveryNum));
|
|
3575
|
+
const maxGenerations = Math.max(0, Math.trunc(maxGenNum));
|
|
3576
|
+
|
|
3577
|
+
// Deterministic 31-bit LCG using BigInt arithmetic (matches the N3 version)
|
|
3578
|
+
let state = BigInt(seed0);
|
|
3579
|
+
const MOD = 2147483648n; // 2^31
|
|
3580
|
+
const A = 1103515245n;
|
|
3581
|
+
const C = 12345n;
|
|
3582
|
+
|
|
3583
|
+
function rnd() {
|
|
3584
|
+
state = (A * state + C) % MOD;
|
|
3585
|
+
return Number(state) / 2147483648;
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
function randRound(N) {
|
|
3589
|
+
return Math.round(rnd() * N);
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
function randomAlpha() {
|
|
3593
|
+
const p = randRound(26);
|
|
3594
|
+
return p === 0 ? ' ' : String.fromCharCode(64 + p);
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
function randomText(len) {
|
|
3598
|
+
let out = '';
|
|
3599
|
+
for (let i = 0; i < len; i++) out += randomAlpha();
|
|
3600
|
+
return out;
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
function mutate(str) {
|
|
3604
|
+
let out = '';
|
|
3605
|
+
for (let i = 0; i < str.length; i++) {
|
|
3606
|
+
const p = randRound(100);
|
|
3607
|
+
if (p > mutProb) out += str[i];
|
|
3608
|
+
else out += randomAlpha();
|
|
3609
|
+
}
|
|
3610
|
+
return out;
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
function score(str) {
|
|
3614
|
+
let diffs = 0;
|
|
3615
|
+
for (let i = 0; i < target.length; i++) if (str[i] !== target[i]) diffs++;
|
|
3616
|
+
return diffs;
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
const pref = trace.getTracePrefixes() || PrefixEnv.newDefault();
|
|
3620
|
+
function doTrace(gen, sc, val) {
|
|
3621
|
+
const genLit = internLiteral(String(gen));
|
|
3622
|
+
const scLit = internLiteral(String(sc));
|
|
3623
|
+
const valLit = makeStringLiteral(val);
|
|
3624
|
+
const obj = new ListTerm([scLit, valLit]);
|
|
3625
|
+
const xStr = termToN3(genLit, pref);
|
|
3626
|
+
const yStr = termToN3(obj, pref);
|
|
3627
|
+
trace.writeTraceLine(`${xStr} TRACE ${yStr}`);
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
let generation = 0;
|
|
3631
|
+
let current = randomText(target.length);
|
|
3632
|
+
let currentScore = score(current);
|
|
3633
|
+
|
|
3634
|
+
if (traceEvery > 0 && generation % traceEvery === 0) doTrace(generation, currentScore, current);
|
|
3635
|
+
|
|
3636
|
+
while (currentScore !== 0) {
|
|
3637
|
+
if (maxGenerations > 0 && generation >= maxGenerations) break;
|
|
3638
|
+
|
|
3639
|
+
let best = '';
|
|
3640
|
+
let bestScore = Infinity;
|
|
3641
|
+
|
|
3642
|
+
for (let i = 0; i < samples; i++) {
|
|
3643
|
+
const cand = mutate(current);
|
|
3644
|
+
const candScore = score(cand);
|
|
3645
|
+
if (candScore < bestScore) {
|
|
3646
|
+
best = cand;
|
|
3647
|
+
bestScore = candScore;
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
generation += 1;
|
|
3652
|
+
current = best;
|
|
3653
|
+
currentScore = bestScore;
|
|
3654
|
+
|
|
3655
|
+
if (traceEvery > 0 && generation % traceEvery === 0) doTrace(generation, currentScore, current);
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
const outList = new ListTerm([
|
|
3659
|
+
internLiteral(String(generation)),
|
|
3660
|
+
internLiteral(String(currentScore)),
|
|
3661
|
+
makeStringLiteral(current),
|
|
3662
|
+
internLiteral(String(seed0)),
|
|
3663
|
+
]);
|
|
3664
|
+
|
|
3665
|
+
const s2 = unifyTerm(g.o, outList, subst);
|
|
3666
|
+
return s2 !== null ? [s2] : [];
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3466
3669
|
// Unknown builtin
|
|
3467
3670
|
return [];
|
|
3468
3671
|
}
|
|
@@ -3483,6 +3686,11 @@ function isBuiltinPred(p) {
|
|
|
3483
3686
|
return true;
|
|
3484
3687
|
}
|
|
3485
3688
|
|
|
3689
|
+
// Eyeling extension: GA demo builtins
|
|
3690
|
+
if (v === 'urn:eyeling:ga:solveString') {
|
|
3691
|
+
return true;
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3486
3694
|
return (
|
|
3487
3695
|
v.startsWith(CRYPTO_NS) ||
|
|
3488
3696
|
v.startsWith(MATH_NS) ||
|
package/lib/builtins.js
CHANGED
|
@@ -3451,6 +3451,209 @@ 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
|
+
// -----------------------------------------------------------------
|
|
3539
|
+
// 4.6.2 urn:eyeling:ga: builtins (fast GA on *strings*)
|
|
3540
|
+
// -----------------------------------------------------------------
|
|
3541
|
+
|
|
3542
|
+
// urn:eyeling:ga:solveString
|
|
3543
|
+
// Schema: ( $target $mutProb $samples $seed $traceEvery $maxGenerations ) :solveString ( $gen $score $value $seed )
|
|
3544
|
+
// Side effect: if traceEvery > 0, prints: <gen> TRACE (<score> "<value>")
|
|
3545
|
+
if (pv === 'urn:eyeling:ga:solveString') {
|
|
3546
|
+
if (!(g.s instanceof ListTerm) || g.s.elems.length != 6) return [];
|
|
3547
|
+
|
|
3548
|
+
const target = termToJsXsdStringNoLang(g.s.elems[0]);
|
|
3549
|
+
const mutProbNum = parseNum(g.s.elems[1]);
|
|
3550
|
+
const samplesNum = parseNum(g.s.elems[2]);
|
|
3551
|
+
const seedNum = parseNum(g.s.elems[3]);
|
|
3552
|
+
const traceEveryNum = parseNum(g.s.elems[4]);
|
|
3553
|
+
const maxGenNum = parseNum(g.s.elems[5]);
|
|
3554
|
+
|
|
3555
|
+
if (target === null || mutProbNum === null || samplesNum === null || seedNum === null) return [];
|
|
3556
|
+
if (traceEveryNum === null || maxGenNum === null) return [];
|
|
3557
|
+
|
|
3558
|
+
const mutProb = Math.max(0, Math.trunc(mutProbNum));
|
|
3559
|
+
const samples = Math.max(1, Math.trunc(samplesNum));
|
|
3560
|
+
const seed0 = Math.trunc(seedNum);
|
|
3561
|
+
|
|
3562
|
+
const traceEvery = Math.max(0, Math.trunc(traceEveryNum));
|
|
3563
|
+
const maxGenerations = Math.max(0, Math.trunc(maxGenNum));
|
|
3564
|
+
|
|
3565
|
+
// Deterministic 31-bit LCG using BigInt arithmetic (matches the N3 version)
|
|
3566
|
+
let state = BigInt(seed0);
|
|
3567
|
+
const MOD = 2147483648n; // 2^31
|
|
3568
|
+
const A = 1103515245n;
|
|
3569
|
+
const C = 12345n;
|
|
3570
|
+
|
|
3571
|
+
function rnd() {
|
|
3572
|
+
state = (A * state + C) % MOD;
|
|
3573
|
+
return Number(state) / 2147483648;
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
function randRound(N) {
|
|
3577
|
+
return Math.round(rnd() * N);
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3580
|
+
function randomAlpha() {
|
|
3581
|
+
const p = randRound(26);
|
|
3582
|
+
return p === 0 ? ' ' : String.fromCharCode(64 + p);
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3585
|
+
function randomText(len) {
|
|
3586
|
+
let out = '';
|
|
3587
|
+
for (let i = 0; i < len; i++) out += randomAlpha();
|
|
3588
|
+
return out;
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
function mutate(str) {
|
|
3592
|
+
let out = '';
|
|
3593
|
+
for (let i = 0; i < str.length; i++) {
|
|
3594
|
+
const p = randRound(100);
|
|
3595
|
+
if (p > mutProb) out += str[i];
|
|
3596
|
+
else out += randomAlpha();
|
|
3597
|
+
}
|
|
3598
|
+
return out;
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
function score(str) {
|
|
3602
|
+
let diffs = 0;
|
|
3603
|
+
for (let i = 0; i < target.length; i++) if (str[i] !== target[i]) diffs++;
|
|
3604
|
+
return diffs;
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
const pref = trace.getTracePrefixes() || PrefixEnv.newDefault();
|
|
3608
|
+
function doTrace(gen, sc, val) {
|
|
3609
|
+
const genLit = internLiteral(String(gen));
|
|
3610
|
+
const scLit = internLiteral(String(sc));
|
|
3611
|
+
const valLit = makeStringLiteral(val);
|
|
3612
|
+
const obj = new ListTerm([scLit, valLit]);
|
|
3613
|
+
const xStr = termToN3(genLit, pref);
|
|
3614
|
+
const yStr = termToN3(obj, pref);
|
|
3615
|
+
trace.writeTraceLine(`${xStr} TRACE ${yStr}`);
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
let generation = 0;
|
|
3619
|
+
let current = randomText(target.length);
|
|
3620
|
+
let currentScore = score(current);
|
|
3621
|
+
|
|
3622
|
+
if (traceEvery > 0 && generation % traceEvery === 0) doTrace(generation, currentScore, current);
|
|
3623
|
+
|
|
3624
|
+
while (currentScore !== 0) {
|
|
3625
|
+
if (maxGenerations > 0 && generation >= maxGenerations) break;
|
|
3626
|
+
|
|
3627
|
+
let best = '';
|
|
3628
|
+
let bestScore = Infinity;
|
|
3629
|
+
|
|
3630
|
+
for (let i = 0; i < samples; i++) {
|
|
3631
|
+
const cand = mutate(current);
|
|
3632
|
+
const candScore = score(cand);
|
|
3633
|
+
if (candScore < bestScore) {
|
|
3634
|
+
best = cand;
|
|
3635
|
+
bestScore = candScore;
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
generation += 1;
|
|
3640
|
+
current = best;
|
|
3641
|
+
currentScore = bestScore;
|
|
3642
|
+
|
|
3643
|
+
if (traceEvery > 0 && generation % traceEvery === 0) doTrace(generation, currentScore, current);
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3646
|
+
const outList = new ListTerm([
|
|
3647
|
+
internLiteral(String(generation)),
|
|
3648
|
+
internLiteral(String(currentScore)),
|
|
3649
|
+
makeStringLiteral(current),
|
|
3650
|
+
internLiteral(String(seed0)),
|
|
3651
|
+
]);
|
|
3652
|
+
|
|
3653
|
+
const s2 = unifyTerm(g.o, outList, subst);
|
|
3654
|
+
return s2 !== null ? [s2] : [];
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3454
3657
|
// Unknown builtin
|
|
3455
3658
|
return [];
|
|
3456
3659
|
}
|
|
@@ -3471,6 +3674,11 @@ function isBuiltinPred(p) {
|
|
|
3471
3674
|
return true;
|
|
3472
3675
|
}
|
|
3473
3676
|
|
|
3677
|
+
// Eyeling extension: GA demo builtins
|
|
3678
|
+
if (v === 'urn:eyeling:ga:solveString') {
|
|
3679
|
+
return true;
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3474
3682
|
return (
|
|
3475
3683
|
v.startsWith(CRYPTO_NS) ||
|
|
3476
3684
|
v.startsWith(MATH_NS) ||
|