-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmorph.red
1315 lines (1173 loc) · 41.7 KB
/
morph.red
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
Red [
title: "MORPH DSL"
purpose: "Dialect for persistent local series mapping"
author: @hiiamboris
license: BSD-3
provides: morph
usage: {
See https://codeberg.org/hiiamboris/red-common/src/master/morph.md
}
]
#include %debug.red
#include %with.red
#include %assert.red
; #include %error-macro.red
; #include %reshape.red
; #include %catchers.red
; #include %selective-catch.red
; #include %reactor92.red
; #include %map-each.red
; do/expand [
; #debug on
;@@ TODO: morph input inp-rule (to map! rule) should not wrap it into a block
;@@ TODO: use interpreter for function rules (but not routines) - favoring simplicity over speed
;@@ TODO: block! rule should fail if it doesn't reach the tail of it!
;@@ and it should only work for lists, not fallback to paren!
;@@ TODO: ability to return the tree without any out-rules (and any inside data.. just the tree)
;@@ TODO: base tree on doubly-linked lists maybe?? to avoid index updates problem (see shift-tree)
morph-ctx: context [
;;============ temporary debugging crap ============
;; `probe` variant
^: probe: function [x [any-type!]] [
if any [block? :x object? :x] [x: replace-vectors-with-paths copy/deep x]
system/words/probe :x
]
;; `??` variant
?!: function ['x [any-type!]] [
if any [any-word? x any-path? x] [set/any 'x get/any name: x]
if any [block? :x object? :x] [x: replace-vectors-with-paths copy/deep x]
if name [prin name prin ": "]
system/words/probe :x
]
;; used for debug output to make it readable
replace-vectors-with-paths: function [b [block! object!]] [
either object? b [
foreach w words-of b [
v: select b w
attempt [replace-vectors-with-paths v]
if vector? :v [put b w to path! to [] v]
]
][
forall b [
attempt [replace-vectors-with-paths b/1]
if vector? :b/1 [
nl: new-line? b
b/1: to path! to [] b/1 ;-- this destroys newline markers
new-line b nl ;-- workaround
]
]
]
b
]
;;============ common stuff used by scanner & emitter ============
FAIL: -1x0
match?: func [p [pair!]][p/1 >= 0]
advanced?: func [p [pair!]][p/1 > 0]
change-last: function [s [series!] x [any-type!]] [
change/only back tail s :x
]
;;============ scanner definition ============
scanner: context [
; ;@@ copy/deep does not copy inner maps unfortunately, so have to use this
; copy-deep-map: function [m [map!]] [
; m: copy/deep m
; foreach [k v] m [if map? :v [m/:k: copy-deep-map v]]
; m
; ]
;@@ hate the number of arguments here, needs reduction
keep-item: function [input args output data name [word!] inner iofs aofs] [
unless locs: select output name [repend output [name locs: copy []]]
pi2: copy pi: copy data/paths/input
change-last pi base: -1 + index? input
change-last pi2 iofs + base
pr2: copy pr: copy data/paths/scan-rules
change-last pr base: -2 + index? args ;-- -2 accounts for the token itself also
change-last pr2 aofs + base
mark: tail locs
repend locs [pi pi2 pr pr2 inner none none none none]
#debug [print ["kept" name mold inner reduce [pi pi2]]]
new-line mark yes
new-line inner yes
]
;;============ scanner actions per token type ============
type-rules: construct compose [
word!: (function [input token args output data /local value] [
#assert [not find [| ...] token] ;-- handled outside, because there's also backtracking logic involved
; name: args/-1
; if find/case [| ...] name [return as-pair 0 length? args]
name: token
set/any 'value any [
select data/scan-dict name
get/any token
]
; type: type?/word :value
handler: any [
unless any-word? :value [ ;-- words referring to lit/set/normal-words get literal treatment
select type-rules type: type?/word :value
]
select type-rules 'any-type!
]
either find [block! paren!] type [
; #debug [print ["INSIDE" name]]
result: do copy/deep [handler/with input :value args output data name] ;@@ #4584 workaround
][ result: handler input :value args output data
]
result
])
;@@ get-word may make it possible to turn word! into rewrite-macro later
;@@ that would put `:get-word` or `set-word: block` in place of the word
get-word!: (function [input token args output data] [ ;-- :rule - always literally treats the referred value
end: find/match/tail input get/any token
1x0 * either end [offset? input end][-1]
])
lit-word!: (function [input token args output data] [ ;-- 'rule
either empty? input [ ;-- 'x fails only if input is empty
-1x0
][
name: to word! token
set/any name :input/1 ;@@ should we have this? for `? x <> y` kind of rules
keep-item input args output data name copy [] 1 1
1x0
]
])
block!: (function [input token args output data /with name] [
either any-list? :input/1 [ ;-- block is able to dive into blocks in the input
append data/paths/input 0
result: type-rules/paren!/with input/1 token args output data name
result/1: either match? result [1][-1] ;-- skip just the list, even if block is empty
take/last data/paths/input
][
;@@ should it fail in list mode when not at the block?
result: type-rules/paren!/with input token args output data name
]
result
])
paren!: (function [input token args output data /with name] [ ;-- (rule)/[rule]
either none? name [ ;-- unnamed block/paren - uses same data
input': eval-ruleset input token output data
][ ;-- set-word or word referring to a block/paren
; token: get name
append data/paths/tree name
inner: copy []
if input': eval-ruleset input token inner data [
keep-item input args output data name inner offset? input input' 1
]
take/last data/paths/tree
]
1x0 * either input' [offset? input input'][-1] ;-- (x)/[x] fail as a whole
])
;@@ incomplete! has to anonymize the word and set it to the result of next expression
set-word!: (function [input token args output data] [ ;-- x: rule
name: to word! token
; #debug [print ["INSIDE" name]]
append data/paths/tree name
inner: copy []
offset: eval-next-rule input args inner data ;-- has to be recursive to support multiple set-words
if match? offset [
keep-item input args output data name inner offset/1 offset/2
]
take/last data/paths/tree
offset
])
;@@ TODO: natives & actions could use a simpler interface:
;@@ take input (and maybe args/1, depending on their arity), return modified input
; native!: (function [input token args output data] [type-rules/routine! input :token args output data])
; action!: (function [input token args output data] [type-rules/routine! input :token args output data])
function!: (function [input token args output data] [type-rules/routine! input :token args output data])
routine!: (function [input token args output data /local offset] [
(set/any 'offset token input args output data) ;-- call :token func; paren ensures no arity spillage
#assert [pair? :offset "scan rules should return pair! value"] ;@@ TODO: macros
offset
; paren? :new [ ;-- macro returned result of it's expansion
; #assert [
; not empty? new
; any [block? :new/1 paren? :new/1 integer? :new/1] "scan macros should return block or integer as rule"
; ]
; rule': either integer? new/1 [skip rule' new/1][new/1]
; expanded: next new
; rule': append copy expanded rule' ;-- concat expanded result with the unprocessed rest of the rule
; [input rule']
; ]
])
bitset!: (function [input token] [
either all [
any-string? input
not tail? input
find token :input/1
] [1x0][-1x0] ;-- matches only in strings
])
datatype!: (function [input token] [
either all [
any-block? input
not tail? input
token = type? :input/1
] [1x0][-1x0]
])
any-type!: (function [input token [any-type!]] [ ;-- catch-all special case
end: find/match/tail input :token
1x0 * either end [offset? input end][-1]
])
]
;;============ scanner core ============
eval-next-rule: function [
input [series!]
rule [block! paren!]
output [block!]
data [object!]
;; returns (ate x used) pair
][
#assert [not unset? :rule/1]
#debug [print ["at" mold/only/flat rule]]
token: :rule/1
handler: any [
select type-rules type?/word :token ;@@ how to avoid hash lookup?
select type-rules 'any-type!
]
args: next rule
; mark: tail output
saved: tree-snapshot output
(offset: handler input :token args output data) ;-- parens prevent arity spillage, just in case
#assert [pair? offset]
;@@ restore when at tail??
; ?? saved
; ?? offset
if offset/1 <= 0 [tree-restore output saved] ;-- do not save results from look-ahead rules
; if offset/1 <= 0 [clear mark] ;-- do not save results from look-ahead rules
offset + 0x1 ;-- count token too
]
eval-ruleset: function [
input [series!]
ruleset [block! paren!] ;-- "set" because `|` allows to have multiple alternatives
output [block!]
data [object!]
;; returns new input offset or none
][
append data/paths/scan-rules 0 ;-- deepen rule path
loop?: '... == last ruleset
rule-start: ruleset
result: forever [
start: input
ruleset: rule-start
matched?: yes ;-- empty rule succeeds
; tail-tree output ;-- mark a position for backtracking
saved: tree-snapshot output ;-- mark a position for backtracking
while [all [not tail? ruleset not tail? input]] [
#assert [not unset? :ruleset/1]
token: :ruleset/1
if find/case [| ...] :token [break]
new: eval-next-rule input ruleset output data
#debug [print [match? new "at" mold input]]
either not match? new [
input: start ;-- input is reset for next alternative
; clear mark ;-- backtrack (when some rule succeeds but later one fails)
; ?? new
; ?? saved
; print ["failed to match" mold ruleset]
; print ["backtracking from" mold input]
;@@ restore when at tail?
tree-restore output saved ;-- backtrack (when some rule succeeds but later one fails)
; clear-tree output ;-- backtrack (when some rule succeeds but later one fails)
unless ruleset: find/case/tail ruleset '| [
matched?: no
break
]
][
; set [input: ruleset:] new ;-- input is only advanced on match
input: skip input new/1 ;-- commit new offsets
ruleset: skip ruleset new/2
change-last data/paths/input -1 + index? input ;-- update input location
change-last data/paths/scan-rules -1 + index? ruleset ;-- update rules location
]
]
; #debug [print [matched? ":" mold start "->" mold input]]
either loop? [ ;-- loop never fails, but ends when doesn't advance or when no match
if any [
not matched?
not advanced?: positive? offset? start input
] [break/return input]
][
break/return if matched? [input]
]
]
take/last data/paths/scan-rules ;-- return to the previous rule nesting level
result
]
scan: function [ ;-- conflicts with lexer's `scan`, so has to be in it's own context
"Parse the INPUT with a given scan RULE"
input [series!]
rule [block! paren!] "Uses scanner's type rules"
dict [map!] "Default dictionary of scanner rules"
/from "STUB: pick up scanning from given offsets" ;@@
input-path [vector!] scan-path [vector!]
/trace "STUB: report each scan result to the trace function" ;@@
tfunc [function! routine!]
][
data: object [
;@@ TODO: deeply copy rules, anonymize (fix) all words in there
;@@ so rules are preserved from accidental modification for morph/live
;@@ perhaps we'll need to do that for macros as well
input: none
scan-dict: dict
scan-rules: rule
tree: copy []
emit-dict: none
emit-rules: none ;-- filled by emitter
output: none
paths: object [
input: make vector! [0]
scan-rules: make vector! []
tree: copy []
emit-rules: none ;-- filled by emitter
output: none
]
]
data/input: input ;-- required for emitter to take slices of
input: eval-ruleset input rule data/tree data
reset-tree data/tree ;-- reset all tree branches to heads (were used for backtracking)
#debug [?! data]
data
]
];; end of scanner
;@@ TODO: macro design
; macro: func [spec body] [
; function spec compose/only [as paren! catch-return (body)]
; ]
;;============ emitter definition ============
emitter: context [
;;============ emitter actions per token type ============
type-rules: construct compose [
;; polymorphic: calls rule functions or dives into named rule blocks
word!: (function [input token args output data /local value] [
#assert [not find [| ...] token] ;-- handled outside, because there's also backtracking logic involved
; name: args/-1
; if find/case [| ...] name [return as-pair 0 length? args]
name: token
set/any 'value any [
select data/emit-dict name
get/any name
]
handler: any [
unless any-word? :value [ ;-- words referring to lit/set/normal-words get literal treatment
select type-rules type: type?/word :value
]
select type-rules 'any-type!
]
either find [block! paren!] type [
result: do copy/deep [handler/with input :value args output data name] ;@@ #4584 workaround
][ result: handler input :value args output data
]
result
])
;@@ incomplete but useful!
set-word!: (function [input token args output data] [ ;-- x: rule
name: to word! token
#assert [any-list? :args/1] ;@@ only x: [group] is supported for now; need more?
handler: select type-rules type: type?/word args/1
0x1 + do copy/deep [handler/with input args/1 (next args) output data name] ;@@ #4584 workaround
;@@ can this be made recursive and support multiple set-words in a chain? is it useful?
])
get-word!: (function [input token args output] [ ;-- :x - emits word's value
append/only output get/any token
1x0 ;-- always succeeds, even if unset
])
lit-word!: (function [input token args output data] [ ;-- 'rule - emits a named value
name: to word! token
unless all [
pos: find/tail input name ;-- named branch exists
not tail? locs: first pos ;-- it's not exhausted
][return FAIL]
#assert [block? locs]
set [ipath1: ipath2:] locs
pos/1: skip locs 9
in1: deep-select data/input ipath1
in2: deep-select data/input ipath2
; ?? data/input ?? ipath
#assert [in1]
#assert [in2]
pos: tail output
append output
set name copy/part in1 in2 ;-- no /only in case we're copying parts of block into other block
locs/7: locs/6: copy data/paths/output
change-last locs/7 (last locs/6) + length? pos
locs/9: locs/8: copy data/paths/emit-rules
change-last locs/9 1
1x0
])
block!: (function [input token args output data /with name] [ ;-- block is able to push blocks into the output
either any-list? output [
inner: copy []
append data/paths/output 0
result: type-rules/paren!/with input token args inner data name
if match? result [append/only output inner]
take/last data/paths/output
][
result: type-rules/paren!/with input token args output data name
]
result
])
;@@ this should become a macro?
paren!: (function [input token args output data /with name] [ ;-- (rule)/[rule]
either none? name [ ;-- unnamed block/paren
result: eval-ruleset input token output data
][ ;-- set-word or word referring to a block/paren
; token: get name
if all [
pos: find/tail input name
locs: first pos
not empty? locs
][
input: fifth locs ;-- ignores this item's bounds, uses content only
#assert [block? input]
end: tail output
append data/paths/tree name
result: eval-ruleset input token output data
take/last data/paths/tree
change/only pos skip locs 9 ;-- consider this item used ;@@ TODO: when to reset tree indexes?
;@@ copy is a workaround for #4913 crash
locs/7: copy locs/6: copy data/paths/output
change-last locs/7 (last locs/6) + length? end
locs/9: copy locs/8: copy data/paths/emit-rules
change-last locs/9 1
]
]
1x0 * either result [1][-1]
])
;@@ TODO: natives & actions could use a simpler interface:
;@@ take output (and maybe args/1, depending on their arity), return modified output
; native!: (function [input token args output data] [type-rules/routine! input :token args output data])
; action!: (function [input token args output data] [type-rules/routine! input :token args output data])
function!: (function [input token args output data] [type-rules/routine! input :token args output data])
routine!: (function [input token args output data /local offset] [
(set/any 'offset token input args output data) ;-- call :token func; paren ensures no arity spillage
#assert [pair? :offset "emit rules should return pair! value"] ;@@ TODO: macros
offset
])
any-type!: (func [input token args output] [ ;-- catch-all special case
append/only output :token
1x0
])
]
;;============ emitter core ============
eval-next-rule: function [
input [block!]
rule [block! paren!]
output [series!]
data [object!]
;; returns (ate x used) pair
][
#assert [not unset? :rule/1]
#debug [print ["at" mold/only/flat rule]]
token: :rule/1
handler: any [
select type-rules type?/word :token
select type-rules 'any-type!
]
args: next rule
saved: copy input
mark: tail output
(offset: handler input :token args output data) ;-- parens prevent arity spillage, just in case
#assert [pair? offset]
if offset/1 <= 0 [
change clear input saved ;-- do not advance input on look-ahead rules
clear mark ;-- do not keep output items from look-ahead rules
]
; ?? offset ?! saved ?! input
offset + 0x1 ;-- count token too
]
;; returns none if fails, not none otherwise
eval-ruleset: function [input ruleset output data] [
append data/paths/emit-rules 0 ;-- deepen rule path
loop?: '... == last ruleset
rule-start: ruleset
result: forever [ ;@@ preprocess the loops for faster iteration? (split into alt-blocks..)
end: tail output
ruleset: rule-start
saved: copy input
matched?: yes ;-- empty rule succeeds
while [not tail? ruleset] [
#assert [not unset? :ruleset/1]
token: :ruleset/1
if find/case [| ...] :token [break]
new: eval-next-rule input ruleset output data
either not match? new [
clear end ;-- backtrack output from any failed rules
change clear input saved ;-- backtrack input indexes too
unless ruleset: find/case/tail ruleset '| [
matched?: no
break
]
][
; input: skip input new/1
ruleset: skip ruleset new/2
; change-last data/paths/input -1 + index? input ;-- update input location
change-last data/paths/output length? output ;-- update output location
change-last data/paths/emit-rules -1 + index? ruleset ;-- update rules location
]
]
#debug [print [matched? ":" mold output]]
either loop? [ ;-- loop never fails, but ends when doesn't advance or when no match
if any [
not matched?
all [
not grown?: positive? length? end
not advanced?: saved <> input
]
] [break/return input]
][
break/return if matched? [input]
]
]
;@@ TODO: eval logic is general enough to be taken out and unified for both scan & emit
take/last data/paths/emit-rules ;-- return to the previous rule nesting level
result
]
emit: function [
"Run emit RULE against scanned DATA and return produced result"
data [object!] "Result of previous SCAN call"
rule [block! paren!] "Uses emitter's type rules"
dict [map!] "Default dictionary of emitter rules"
/into output [series!] "Specify a target to append to (default: new block)"
/from "STUB: pick up emission from given offsets" ;@@
output-path [vector!] emit-path [vector!]
/trace "STUB: report each emit result to the trace function" ;@@
tfunc [function! routine!]
][
output: any [output copy []]
; data/emit-rules: copy/deep rule
data/emit-dict: dict
data/emit-rules: rule
data/output: output
data/paths/emit-rules: make vector! []
data/paths/output: make vector! [0]
eval-ruleset data/tree rule output data
reset-tree data/tree
#debug [?! data]
output
]
];; end of emitter
;;============ tree manipulation helpers ============
deep-select: function [data path] [
repeat i length? next path [
unless data: pick data path/:i + 1 [break]
]
if data [skip data last path]
]
reset-tree: function [tree /local x] [
parse tree rule: [any [ahead change only set x block! (head x) into rule | skip]]
]
tree-snapshot: function [tree /local x] [
shot: make hash! collect [
parse tree [any [
word! set x block! (keep/only head x keep/only tail x)
;@@ save inner blocks or not?
]]
]
]
tree-restore: function [tree shot /local x] [
; prin "TREE BEFORE RESTORE: " ?! tree ?! shot
parse tree [any [
p: word! set x block! (
set [hd: tl:] find/same/skip/only shot head x 2
either hd [
; print ["CLEARING" p/1 "at" mold tl]
clear tl
p: skip p 2
][
; print ["REMOVING" p/1]
remove/part p 2
]
) :p
]]
; prin "TREE AFTER RESTORE: " ?! tree
]
;; this doesn't work when an inner loop advances the tree to it's tail:
; tail-tree: function [tree /local x] [
; parse tree [any [
; word! change only set x block! (tail x)
; ]]
; ]
; clear-tree: function [tree /local x] [
; parse tree [any [
; word! set x block! (clear x)
; ]]
; ]
deep-offset?: function [series chunk /with path] [
path: any [path copy []]
if (head series) =? head chunk [return append path offset? series chunk]
#assert [any-list? series]
;; this is dumb and slow but I don't see any other way to locate the inner change
;; see https://github.com/red/red/issues/4524#issuecomment-953570556
;@@ proper way to implement this is to turn it into a loop that fires for each series match
;@@ and for speed we'll have to collect a map of each inner series (at head) to it's deep offset in the source
pos: series
append path 0
while [pos: find/tail pos series!] [
change-last path offset? series back pos
if deep-offset?/with pos/-1 chunk path [return path]
]
take/last path
none
]
; path-inside?: function [single-path double-path] [
; repeat i length? double-path [
; x: single-path/:i
; #assert [integer? x]
; if all [
; pair? p: double-path/:i
; p/1 <= x x <= (p/1 + p/2)
; ] [return yes]
; if p <> x [return no]
; ]
; ]
;; returns input & rule paths to the closest value started before 'path' in input
get-value-before: function [tree [block!] path [vector!]] [
; ?! path ?! tree
foreach [name locs] tree [
for-each [p: sinp einp sscn escn _ _ _ _ _] locs [
if sinp >= path [continue] ;-- value should be strictly < path in case it relies on look-ahead rules
dist: (copy path) - sinp
if any [none? mindist dist < mindist] [
mindist: dist
closest: p
]
if einp >= path [break]
]
; if all [einp einp >= path] [break]
]
if closest [
any [
get-value-before closest/5 path
closest
]
]
]
;; used to remove values started (not ended) within or at any of from/into margins
;; margins are ambiguous: will they extend to new margin or be broken? so I remove inclusively to play it safe
;; memo: no need to remove values that will be replaced during rescan
;; memo: should be called before get-value-before
cut-tree: function [tree [block!] from [vector!] into [vector!]] [
foreach [name locs] tree [
for-each [p: sinp einp _ _ _ _ _ _ _] [ ;@@ TODO: output tree
if all [from <= sinp sinp <= into] [
remove/part p 9
p: skip p -9
continue
]
]
]
]
;; shifts all start/end points after `from` by `into - from`
;; memo: should be called after cut-tree
shift-tree: function [tree [block!] from [vector!] into [vector!]] [
#assert [into <> from]
#assert [(length? into) = length? from]
removal?: into < from
diff: into - from
foreach [name locs] tree [
for-each [p: sinp einp _ _ _ _ _ _ _] [ ;@@ TODO: output tree
if sinp >= from [p/1: sinp + diff]
if einp >= from [p/2: einp + diff]
]
]
]
;;============ primary interface ============
scan: :scanner/scan
emit: :emitter/emit
;@@ limitation: only one mapping per source is possible right now;
;@@ TODO: need to write a dispatching mechanism for anything better
;@@ TODO: how to destroy existing mapping
set 'morph function [
"Transform source into target given a set of rules"
source [series!] "Will become owned by a new anonymous object"
scan-rule [block! paren!] "Rule to interpret the source"
emit-rule [block! paren!] "Rule to produce the target"
/into target [series!]
/custom "Use custom rule dictionaries instead of `scan-rules` and `emit-rules`"
scan-dict [map!] emit-dict [map!]
;@@ eventually /auto should be able to bind the expanded rule, so it will be bound deeply
;@@ and /auto should be the default case, and /manual for when we want maximum juice
/live "Establish a persistent mapping (TBD)"
;; returns target, so even if it's not provided, /live is not in vain
][
target: any [target copy []]
either not live [
data: scan source scan-rule any [scan-dict scan-rules]
emit/into data emit-rule any [emit-dict emit-rules] target
][
do make error! "Not implemented!"
reactor: make deep-reactor-92! [
source: none
target: none
data: none
temp: none
on-deep-change-92*: func [
word [word!] "name of the field value of which is being changed"
pos [series!] "series at removal or insertion point"
part [integer!] "length of removal or insertion"
insert? [logic!] "true = just inserted, false = about to remove"
reordering? [logic!] "removed items won't leave the series, inserted items came from the same series"
; done? [logic!] "signifies that series is in it's final state (after removal/insertion)"
][
if word <> 'source [exit]
#assert [data]
#assert [target]
if all [ ;-- special case: complete rewrite of the source
part = length? pos
pos =? source
][
clear self/target
if insert? [
self/data: scan source self/data/scan-rule
emit/into self/data self/data/emit-rule self/target
]
exit
]
;-- now we're dealing with a partial update
;-- first, we need to locate changed sub-series within 'source' (in case it's a deep change)
change-path: deep-offset? source pos
#assert [change-path]
change-end: copy change-path
done?: any [insert? part = 0] ;-- series is at it's final state after modification?
if part > 0 [ ;-- true before removal and after insertion
either insert? [
change-end: head add back tail change-end part ;-- shift to the right
#assert [done?]
cut-tree data/tree change-path change-path ;-- items at insertion point become invalid
shift-tree data/tree change-path change-end
][
change-path: head add back tail change-path part;-- shift to the left
#assert [not done?] ;-- part should be =0 for done?=yes
;-- remove the no longer valid values from the tree
cut-tree data/tree change-path change-end ;-- whole range of items becomes invalid
shift-tree data/tree change-path change-end
;-- scanning should be done when we have the final series
exit
]
]
;-- now pick up the scan
#assert [done?] ;-- only scan the actualized data
set [input-path: _: scan-path:] get-value-before data/tree change-path
data: scan/from/trace
data/source data/scan-rule
input-path scan-path
function [...] [
;@@ tracing function will receive results of successful match of named values
;@@ will put these results into data/tree, keeping it sorted
;@@ and once it detects a result that is already in the tree, it stops the scan
;@@ it will also mark the locations of each newly inserted item
]
;-- now pick up the emit
emit/into/from/trace
data data/emit-rule
slice: make target part
output-path emit-path
function [...] [
;@@ tracing function should track offsets in emit rules and in the tree
;@@ once all newly added tree items have been processed and
;@@ the next emitted named item aligns with the one present in output (with offset correction)
;@@ we stop the emission and consider the rest of the output valid
]
;@@ merge slice with output, deeply
]
on-change**: :on-change*
on-change*: function [word old [any-type!] new [any-type!]] [
on-change** word :old :new
if word = 'source [
word: to word! word
if series? :old [
on-deep-change-92* word old length? old no no
]
if series? :new [
on-deep-change-92* word new length? new yes no
]
]
]
]
set-quiet in reactor data scan source scan-rule ;-- do initial scan
set-quiet in reactor 'target target
reactor/source: source ;-- transfer ownership
]
target
]
]
;;============ scan rules are fully in defined userspace ============
scan-rules: make map! compose with morph-ctx [ ;-- construct preserves function code from being bound to rule names
?: (function [
"Evaluate next expression, succeed if it's not none or false"
input args /local r end
][
set/any 'r do/next as [] args 'end ;@@ as [] required per #4980
; as-pair either :r [0][-1] offset? args end ;@@ either unset doesn't work
as-pair either any [unset? :r :r] [0][-1] offset? args end
])
do: (function [
"Evaluate next expression and continue"
input args /local r end
][
set/any 'r do/next as [] args 'end ;@@ as [] required per #4980
0x1 * offset? args end
])
??: (func [
"Display value of next token"
input args
][
?? (:args/1)
0x1
])
show: (func [
"Display current input location"
input
][
n: -9 + any [attempt [system/console/size/x] 80]
print ["input:" copy/part mold/part input n n]
0x0
])
opt: (func [
"Try to match next rule, but succeed anyway, similar to (rule |)"
input args output data
][
max 0x0 scanner/eval-next-rule input args output data
])
ahead: (function [
"Look ahead if next rule succeeds"
input args output data
][
new: scanner/eval-next-rule input args output data
as-pair (either match? new [0][-1]) new/2
])
not: (function [
"Look ahead if next rule fails"
input args output data
][
new: scanner/eval-next-rule input args output data
as-pair (either match? new [-1][0]) new/2
])
some: (function [
"Match next rule one or more times"
input args output data
][
offset: scanner/eval-next-rule input args output data
if match? offset [
input: skip input offset/1
change-last data/paths/input -1 + index? input
offset/1: offset/1 + first scan-rules/any input args output data
] ;-- fails if never succeeded
offset
])
any: (function [
"Match next rule zero or more times (always succeeds)"
input args output data
][
;@@ TODO: just call eval-ruleset somehow
offset: 0
while [advanced? new: scanner/eval-next-rule input args output data] [
input: skip input new/1
change-last data/paths/input -1 + index? input
offset: offset + new/1
]
as-pair offset new/2 ;-- never fails
])
quote: (func [
"Match next token literally vs the input"
input args
][
either :input/1 = :args/1 [1x1][-1x1]
])
lit: (function [
"Match contents of next block/paren (or word referring to it) vs input"
input args
][
either end: find/match/tail
input
either word? :args/1 [get/any args/1][:args/1]
[as-pair (offset? input end) 1][-1x1]
])
skip: (func [
"Match any single value"
input
][
either tail? input [FAIL][1x0]
])
head: (func [
"Match head of input only"
input
][
either head? input [0x0][FAIL]
])
tail: (func [
"Match tail of input only"