util  Diff

Differences From Artifact [30400bd2f4]:

To Artifact [af37224306]:


    50     50   		if src == nil then return end
    51     51   		local sc = #src
    52     52   		for j=1,sc do dest[i+j] = src[j] end
    53     53   		i = i + sc
    54     54   		iter(...)
    55     55   	end
    56     56   	iter(...)
           57  +end
           58  +local function fastDelete(table,idx)
           59  +-- delete without preserving table order
           60  +	local l = #table
           61  +	table[idx] = table[l]
           62  +	table[l] = nil
           63  +	return table
    57     64   end
    58     65   local function tcat(...)
    59     66   	local new = {}
    60     67   	tcatD(new, ...)
    61     68   	return new
    62     69   end
    63     70   local ansi = {
................................................................................
   329    336   		decode = function(s)
   330    337   			local n = parse(ty,s)
   331    338   			if (n+1) > #vals then error(string.format('enum "%s" does not have %u members', table.concat(vals,'","'),n),3) end
   332    339   			return vals[n+1]
   333    340   		end;
   334    341   	}
   335    342   end
          343  +
          344  +fmt.uid = fmt.u32
          345  +
          346  +fmt.relatable = function(ty)
          347  +	return tcat(ty,{
          348  +		{'rels',fmt.list(fmt.uid,fmt.u16)};
          349  +	})
          350  +end
   336    351   
   337    352   fmt.note = {
   338    353   	{'kind', fmt.tag};
   339    354   	{'paras', fmt.list(fmt.string)};
   340    355   }
   341    356   
   342    357   fmt.example = {
   343    358   	{'quote',fmt.string};
   344    359   	{'src',fmt.label};
   345    360   }
   346         -fmt.meaning = {
          361  +
          362  +fmt.meaning = fmt.relatable {
   347    363   	{'lit', fmt.string};
   348    364   	{'examples', fmt.list(fmt.example,fmt.u8)};
   349    365   	{'notes', fmt.list(fmt.note,fmt.u8)};
   350    366   }
   351    367   
   352         -fmt.phrase = {
          368  +fmt.phrase = fmt.relatable {
   353    369   	{'str',fmt.label};
   354    370   	{'means',fmt.list(fmt.meaning,fmt.u8)};
   355         -	{'xref',fmt.list(fmt.path,fmt.u16)};
   356    371   }
   357    372   
   358         -fmt.def = {
          373  +fmt.def = fmt.relatable {
   359    374   	{'part', fmt.u8};
   360    375   	{'branch', fmt.list(fmt.label,fmt.u8)};
   361    376   	{'means', fmt.list(fmt.meaning,fmt.u8)};
   362    377   	{'forms', fmt.map(fmt.u16,fmt.label,fmt.u16)};
   363    378   	{'phrases', fmt.list(fmt.phrase,fmt.u16)};
   364    379   }
   365    380   
   366         -fmt.word = {
          381  +fmt.word = fmt.relatable {
   367    382   	{'defs', fmt.list(fmt.def,fmt.u8)};
   368    383   }
   369    384   
   370    385   fmt.dictHeader = {
   371    386   	{'lang', fmt.tag};
   372    387   	{'meta', fmt.string};
   373    388   	{'partsOfSpeech', fmt.list(fmt.tag,fmt.u16)};
................................................................................
   378    393   		{'parts', fmt.list(fmt.tag,fmt.u8)};
   379    394   		-- which parts of speech does this form apply to?
   380    395   		-- leave empty if not relevant
   381    396   	},fmt.u16)};
   382    397   }
   383    398   
   384    399   fmt.relSet = {
   385         -	{'uid', fmt.u32};
          400  +	{'uid', fmt.uid};
   386    401   		-- IDs are persistent random values so they can be used
   387    402   		-- as reliable identifiers even when merging exports in
   388    403   		-- a parvan-unaware VCS
   389         -	{'members', fmt.list(fmt.path,fmt.u16)};
   390    404   	{'kind', fmt.enum('syn','ant','met')};
          405  +	-- membership is stored in individual objects, using a field
          406  +	-- attached by the 'relatable' template
   391    407   }
   392    408   
   393    409   fmt.dict = {
   394    410   	{'header', fmt.dictHeader};
   395    411   	{'words', fmt.map(fmt.string,fmt.word)};
   396    412   	{'relsets', fmt.list(fmt.relSet)};
   397    413   }
................................................................................
   441    457   end
   442    458   
   443    459   local function rebuildRelationCache(d)
   444    460   -- (re)build a dictionary's relation cache; needed
   445    461   -- at load time and whenever any changes to relsets
   446    462   -- are made (unless they're simple enough to update
   447    463   -- the cache directly by hand, but that's very eeeh)
   448         -	local sc = {}
   449         -	for i,s in ipairs(d.relsets) do
   450         -		for j,m in ipairs(s.members) do
   451         -			sc[m.w] = sc[m.w] or {}
   452         -			table.insert(sc[m.w], s)
          464  +	local setMems = {} -- indexed by set id
          465  +	local function scan(obj,path)
          466  +		for _,v in pairs(obj.rels) do
          467  +			setMems[v] = setMems[v] or {mems={}}
          468  +			table.insert(setMems[v].mems, {path=path, obj=obj})
   453    469   		end
   454    470   	end
   455         -	d._relCache = sc
          471  +	for wk,wv in pairs(d.words) do
          472  +		scan(wv, {w=wk})
          473  +		for dk,dv in pairs(wv.defs) do
          474  +			scan(dv, {w=wk, dn=dk})
          475  +			for mk,mv in pairs(dv.means) do
          476  +				scan(mv, {w=wk, dn=dk, mn=mk})
          477  +			end
          478  +			for pk,pv in pairs(dv.phrases) do
          479  +				scan(pv, {w=wk, dn=dk, pn=pk})
          480  +				for mk,mv in pairs(pv.means) do
          481  +					scan(mv, {w=wk, dn=dk, pn=pk, mn=mk})
          482  +				end
          483  +			end
          484  +		end
          485  +	end
          486  +	for sk,sv in pairs(d.relsets) do
          487  +		setMems[sv.uid] = setMems[sv.uid] or {}
          488  +		setMems[sv.uid].set = sv
          489  +	end
          490  +	d._relCache = setMems
   456    491   end
   457    492   
   458    493   local function
   459    494   writeDict(d)
   460    495   	local atomizePoS, posMap = atomizer()
   461    496   	for lit,w in pairs(d.words) do
   462    497   		for j,def in ipairs(w.defs) do
................................................................................
   749    784   		if not fd then error(userError("cannot open file " .. file),2) end
   750    785   		return fd
   751    786   	else
   752    787   		return file
   753    788   	end
   754    789   end
   755    790   
   756         -local function
   757         -safeNavWord(ctx, word, dn, mn, nn)
   758         -	local w = ctx.dict.words[word]
   759         -	if not w then id10t 'bad word' end
   760         -	if dn == nil then return w end
   761         -
   762         -	local d = w.defs[tonumber(dn)]
   763         -	if not d then id10t('no definition #%u',dn) end
   764         -	if mn == nil then return w,d end
   765         -
   766         -	local m = d.means[tonumber(mn)]
   767         -	if not m then id10t('no meaning #%u',mn) end
   768         -	if nn == nil then return w,d,m end
   769         -
   770         -	local n = m.notes[tonumber(nn)]
   771         -	if not n then id10t('no note #%u',nn) end
   772         -	return w,d,m,n
   773         -end
   774         -
   775    791   local function copy(tab)
   776    792   	local new = {}
   777    793   	for k,v in pairs(tab) do new[k] = v end
   778    794   	return new
   779    795   end
   780    796   
   781    797   local function pathParse(p)
................................................................................
   840    856   		and a.dn == b.dn
   841    857   		and a.mn == b.mn
   842    858   		and a.pn == b.pn
   843    859   		and a.nn == b.nn
   844    860   		and a.xn == b.xn
   845    861   end
   846    862   local function pathResolve(ctx, a)
   847         -	if not a.w then return end -- empty paths are valid!
          863  +	local res = {}
          864  +
          865  +	if not a.w then return res end -- empty paths are valid!
   848    866   	local function lookup(seg, tbl,val)
   849    867   		if not tbl then error('bad table',2) end
   850    868   		local v = tbl[val]
   851    869   		if v then return v end
   852    870   		id10t('bad %s in path: %s', seg, val)
   853    871   	end
   854    872   
   855         -	local res = {}
   856    873   	res.word = lookup('word', ctx.dict.words, a.w)
   857    874   	if not a.dn then return res end
   858    875   
   859         -	res.def = lookup('definition', w.defs, a.dn)
          876  +	res.def = lookup('definition', res.word.defs, a.dn)
   860    877   	if (not a.pn) and (not a.mn) then return res end
   861    878   
   862    879   	local m if a.pn then
   863         -		res.phrase = lookup('phrase', d.phrases, a.pn)
   864         -		res.meaning = lookup('meaning', p.means, a.mn)
          880  +		res.phrase = lookup('phrase', res.def.phrases, a.pn)
          881  +		res.meaning = lookup('meaning', res.phrase.means, a.mn)
   865    882   	else
   866         -		res.meaning = lookup('meaning', d.means, a.mn)
          883  +		res.meaning = lookup('meaning', res.def.means, a.mn)
   867    884   	end
   868    885   
   869    886   	if a.xn then
   870         -		res.ex = lookup('example',m.examples,a.xn)
          887  +		res.ex = lookup('example',res.meaning.examples,a.xn)
   871    888   	elseif a.nn then
   872         -		res.note = lookup('note',m.notes,a.nn)
          889  +		res.note = lookup('note',res.meaning.notes,a.nn)
   873    890   	end
   874    891   
   875    892   	return res
   876    893   end
   877    894   
   878    895   local function pathNav(...)
   879    896   	local t = pathResolve(...)
................................................................................
   936    953   		help = "add a new word";
   937    954   		syntax = "<word>";
   938    955   		write = true;
   939    956   		exec = function(ctx,word)
   940    957   			if ctx.dict.words[word] then
   941    958   				id10t "word already coined"
   942    959   			end
   943         -			ctx.dict.words[word] = {defs={}}
          960  +			ctx.dict.words[word] = {defs={},rels={}}
   944    961   		end;
   945    962   	};
   946    963   	def = {
   947    964   		help = "define a word";
   948    965   		syntax = "<word> <part-of-speech> [<meaning> [<root>…]]";
   949    966   		write = true;
   950    967   		exec = function(ctx,word,part,means,...)
   951    968   			local etym = {...}
   952    969   			if (not word) or not part then
   953    970   				id10t 'bad definition'
   954    971   			end
   955    972   			if not ctx.dict.words[word] then
   956         -				ctx.dict.words[word] = {defs={}}
          973  +				ctx.dict.words[word] = {defs={},rels={}}
   957    974   			end
   958    975   			local n = #(ctx.dict.words[word].defs)+1
   959    976   			ctx.dict.words[word].defs[n] = {
   960    977   				part = part;
   961    978   				branch = etym;
   962    979   				means = {means and {
   963    980   					lit=means;
   964    981   					examples={};
   965    982   					notes={};
          983  +					rels={};
   966    984   				} or nil};
   967    985   				forms = {};
          986  +				phrases = {};
          987  +				rels={};
   968    988   			}
   969    989   			ctx.log('info', string.format('added definition #%u to “%s”', n, word))
   970    990   		end;
   971    991   	};
   972    992   	mean = {
   973    993   		help = "add a meaning to a definition";
   974    994   		syntax = "<word> <def#> <meaning>";
................................................................................
  1002   1022   				end
  1003   1023   				local links={}
  1004   1024   				for i,l in ipairs{select(2,...)} do
  1005   1025   					links[i] = pathParse(l)
  1006   1026   				end
  1007   1027   				local newstruct = {
  1008   1028   					uid=math.random(1,0xffffFFFF);
  1009         -					members=links;
  1010   1029   					kind = rel;
  1011   1030   				}
  1012   1031   				table.insert(ctx.dict.relsets, newstruct)
         1032  +				for _, m in pairs(links) do
         1033  +					local obj = pathRef(ctx,m)
         1034  +					table.insert(obj.rels,newstruct.uid)
         1035  +				end
  1013   1036   
  1014         -				local rc = ctx.dict._relCache
  1015         -				for i,l in pairs(links) do
  1016         -					rc[l.w] = rc[l.w] or {}
  1017         -					table.insert(rc[l.w], newstruct)
  1018         -				end
  1019   1037   				rebuildRelationCache(ctx.dict)
  1020   1038   			else -- assemble a list of groups
  1021   1039   				local tgtw = ...
  1022   1040   				local wp = pathParse(tgtw)
  1023         -				local w,d,m = pathNav(ctx, wp)
  1024         -				for i,ss in ipairs(ctx.dict.relsets) do
  1025         -					for j,s in ipairs(ss.members) do
  1026         -						if pathSub(s, wp) then
  1027         --- 						if s.word == wp.w and (wp.dn == nil or s.def == wp.dn) then
  1028         -							table.insert(groups, {set = ss, mem = s, id = i})
         1041  +				local o = pathResolve(ctx, wp)
         1042  +
         1043  +				for i,rs in pairs(ctx.dict.relsets) do
         1044  +					local allMembers = ctx.dict._relCache[rs.uid].mems
         1045  +					for j,s in ipairs(allMembers) do
         1046  +						if pathSub(s.path, wp) then
         1047  +							table.insert(groups, {
         1048  +								set = {
         1049  +									uid = rs.uid;
         1050  +									kind = rs.kind;
         1051  +									members = allMembers;
         1052  +								};
         1053  +								mem = s;
         1054  +								id = i;
         1055  +							})
  1029   1056   							break
  1030   1057   						end
  1031   1058   					end
  1032   1059   				end
  1033   1060   
  1034   1061   				if op == 'show' then
  1035   1062   					for i, g in ipairs(groups) do
................................................................................
  1049   1076   								end
  1050   1077   							end
  1051   1078   
  1052   1079   							return repr
  1053   1080   						end
  1054   1081   
  1055   1082   						local others = {}
  1056         -						for j, o in ipairs(g.set.members) do
         1083  +						for j, oo in ipairs(g.set.members) do
         1084  +							local o = oo.path
  1057   1085   							local ow = pathResolve(ctx, {w=o.w}).w
  1058         -							if (g.set.kind == 'ant' or not pathMatch(o, g.mem)) and
         1086  +							if (g.set.kind == 'ant' or not pathMatch(o, g.mem.path)) and
  1059   1087   								--exclude antonym headwords
  1060   1088   								not (g.set.kind == 'ant' and j==1) then
  1061   1089   								table.insert(others, '      '..label(o,ow))
  1062   1090   							end
  1063   1091   						end
  1064   1092   						local llab do
  1065   1093   							local cdw = ctx.dict.words
  1066   1094   							if g.set.kind == 'ant' then
  1067         -								local ap = g.set.members[1]
         1095  +								local ap = g.set.members[1].path
  1068   1096   								llab = fo.br(label(ap,cdw[ap.w]) or '')
  1069   1097   							else
  1070         -								llab = fo.br(label(g.mem,cdw[g.mem.w]) or '')
         1098  +								llab = fo.br(label(g.mem.path,cdw[g.mem.w]) or '')
  1071   1099   							end
  1072   1100   						end
  1073   1101   						local kls = {
  1074   1102   							syn = fo.color('synonyms',2,true)..' of';
  1075   1103   							ant = fo.color('antonyms',1,true)..' of';
  1076   1104   							met = fo.color('metonyms',4,true)..' of';
  1077   1105   						}
................................................................................
  1082   1110   					local tgtn, paths = (select(2,...)), { select(3, ...) }
  1083   1111   					rebuildRelationCache(ctx.dict)
  1084   1112   				elseif op == 'destroy' then
  1085   1113   					local tgtw, tgtn = ...
  1086   1114   					if not tgtn then id10t 'missing group number' end
  1087   1115   					local delendum = groups[tonumber(tgtn)]
  1088   1116   					if not delendum then id10t 'bad group number' end
  1089         -					local rs = ctx.dict.relsets
  1090         -					local last = #rs
  1091         -					if delendum.id == last then
  1092         -						rs[delendum.id] = nil
  1093         -					else -- since order doesn't matter, we can use a
  1094         -						-- silly swapping trick to reduce the deletion
  1095         -						-- worst case from O(n) to O(2)
  1096         -						rs[delendum.id] = rs[last]
  1097         -						rs[last] = nil
         1117  +
         1118  +					for k,v in pairs(delendum.set.members) do
         1119  +						for idx, e in pairs(v.obj.rels) do
         1120  +							if e == delendum.set.uid then
         1121  +								fastDelete(v.obj.rels,idx)
         1122  +							end
         1123  +						end
  1098   1124   					end
         1125  +					fastDelete(ctx.dict.relsets, delendum.id)
         1126  +
  1099   1127   					rebuildRelationCache(ctx.dict)
  1100   1128   				else
  1101   1129   					id10t 'invalid operation'
  1102   1130   				end
  1103   1131   			end
  1104   1132   		end;
  1105   1133   	};
................................................................................
  1115   1143   		help = "add a note to a definition or a paragraph to a note";
  1116   1144   		syntax = {"(<m-path> (add|for) <kind> | <m-path>:<note#>) <para>…";
  1117   1145   			"m-path ::= <word>@<def#>/<meaning#>"};
  1118   1146   		write = true;
  1119   1147   		exec = function(ctx,path,...)
  1120   1148   			local paras, mng
  1121   1149   			local dest = pathParse(path)
  1122         -			local _,_,m = safeNavWord(ctx,dest.w,dest.dn,dest.mn)
         1150  +			local t = pathResolve(ctx,path)
  1123   1151   			if dest.nn then
  1124   1152   				paras = {...}
  1125   1153   			else
  1126   1154   				local op, kind = ...
  1127   1155   				paras = { select(3, ...) }
  1128   1156   				if op == 'add' then
  1129         -					dest.nn = #(m.notes) + 1
  1130         -					m.notes[dest.nn] = {kind=kind, paras=paras}
         1157  +					dest.nn = #(t.m.notes) + 1
         1158  +					t.m.notes[dest.nn] = {kind=kind, paras=paras}
  1131   1159   					return
  1132   1160   				elseif op == 'for' then
  1133         -					for i,nn in ipairs(m.notes) do
         1161  +					for i,nn in ipairs(t.m.notes) do
  1134   1162   						if nn.kind == kind then
  1135   1163   							dest.nn = i break
  1136   1164   						end
  1137   1165   					end
  1138   1166   					if not dest.nn then
  1139   1167   						id10t('no note of kind %s in %s',kind,path)
  1140   1168   					end
  1141   1169   				end
  1142   1170   			end
  1143         -			local dpa = m.notes[dest.nn].paras
         1171  +			local dpa = t.m.notes[dest.nn].paras
  1144   1172   			local top = #dpa
  1145   1173   			for i,p in ipairs(paras) do
  1146   1174   				dpa[top+i] = p
  1147   1175   			end
  1148   1176   		end
  1149   1177   	};
  1150   1178   	shell = {
................................................................................
  1216   1244   		end
  1217   1245   	end
  1218   1246   	table.sort(out, function(a,b) return a.lit < b.lit end)
  1219   1247   	local fo = ctx.sty[io.stdout]
  1220   1248   
  1221   1249   	local function gatherRelSets(path)
  1222   1250   		local antonymSets, synonymSets, metonymSets = {},{},{}
  1223         -		if ctx.dict._relCache[path.w] then
  1224         -			for i, rel in ipairs(ctx.dict._relCache[path.w]) do
         1251  +		local obj = pathRef(ctx,path)
         1252  +		if next(obj.rels) then
         1253  +			for i, relid in ipairs(obj.rels) do
  1225   1254   				local specuset,tgt,anto = {}
  1226         -				for j, mbr in ipairs(rel.members) do
  1227         -					if pathMatch(mbr, path) then
         1255  +				local rel = ctx.dict._relCache[relid].set
         1256  +				for j, mbr in ipairs(ctx.dict._relCache[relid].mems) do
         1257  +					if pathMatch(mbr.path, path) then
  1228   1258   						if     rel.kind == 'syn' then tgt = synonymSets
  1229   1259   						elseif rel.kind == 'met' then tgt = metonymSets
  1230   1260   						elseif rel.kind == 'ant' then
  1231   1261   							if j == 1 -- is this the headword?
  1232   1262   								then tgt = antonymSets
  1233   1263   								else tgt = synonymSets
  1234   1264   							end
  1235   1265   						end
  1236   1266   					elseif j == 1 and rel.kind == 'ant' then
  1237         -						anto = mbr
         1267  +						anto = mbr.path
  1238   1268   					else
  1239         -						table.insert(specuset, mbr)
         1269  +						table.insert(specuset, mbr.path)
  1240   1270   					end
  1241   1271   				end
  1242   1272   				if tgt then
  1243   1273   					table.insert(tgt, specuset)
  1244   1274   					if anto then
  1245   1275   						table.insert(antonymSets, {anto})
  1246   1276   					end
................................................................................
  1327   1357   	end
  1328   1358   	for i, w in ipairs(out) do
  1329   1359   		local d = fo.ul(fo.br(w.lit))
  1330   1360   		local wordrels = autobreak(table.concat(
  1331   1361   				formatRels(gatherRelSets{w=w.lit}, 2),
  1332   1362   				'\n'
  1333   1363   			))
  1334         -		local wc = ctx.dict._relCache[w.lit]
  1335   1364   		if #w.word.defs == 1 then
  1336   1365   			d=d .. ' '
  1337   1366   			    .. fo.rgb(fo.em('('..(w.word.defs[1].part)..')'),.8,.5,1) .. '\n'
  1338   1367   			    .. meanings(w,w.word.defs[1],false,1) .. '\n'
  1339   1368   			    .. autobreak(table.concat(formatRels(gatherRelSets{w=w.lit,dn=1}, 4), '\n'))
  1340   1369   				 .. wordrels .. '\n'
  1341   1370   		else
................................................................................
  1393   1422   				if c ~= 'pv0' then
  1394   1423   					id10t "not a parvan export"
  1395   1424   				end
  1396   1425   				new.header.lang = words[2]
  1397   1426   				new.header.meta = words[3]
  1398   1427   				state = 1
  1399   1428   			else
  1400         -				print(pathString(path, ctx.sty[io.stderr]))
  1401         -				local W,D,M,N = pathNav({dict=new}, path)
         1429  +				local T = pathResolve({dict=new}, path)
         1430  +				local W,D,P,M,N,X =
         1431  +					T.word,
         1432  +					T.def,
         1433  +					T.phrase,
         1434  +					T.meaning,
         1435  +					T.note,
         1436  +					T.ex
  1402   1437   				if c == 'w' then syn(1) state = 2
  1403   1438   					path = {w=words[2]}
  1404         -					new.words[words[2]] = {defs={}}
         1439  +					new.words[words[2]] = {defs={},rels={}}
  1405   1440   				elseif c == 'f' then syn(1)
  1406   1441   					local nf = {
  1407   1442   						name = words[2];
  1408   1443   						abbrev = words[3] or "";
  1409   1444   						desc = words[4] or "";
  1410   1445   						parts = {};
  1411   1446   					}
................................................................................
  1418   1453   					table.insert(lastinfl.parts,words[2])
  1419   1454   				elseif c == 's' then syn(2)
  1420   1455   					relsets[words[3]] = relsets[words[3]] or {}
  1421   1456   					relsets[words[3]].kind = words[2]
  1422   1457   					relsets[words[3]].uid = tonumber(words[3])
  1423   1458   					relsets[words[3]].members = relsets[words[3]].members or {}
  1424   1459   				elseif state >= 2 and c == 'r' then syn(1)
         1460  +					local rt
         1461  +					if state == 2 then
         1462  +						rt = W.rels
         1463  +					elseif state == 3 then
         1464  +						rt = D.rels
         1465  +					elseif state == 4 then
         1466  +						rt = D.rels
         1467  +					elseif state == 14 then
         1468  +						rt = P.rels
         1469  +					end
  1425   1470   					relsets[words[2]] = relsets[words[2]] or {
  1426         -						uid = tonumber(words[2]);
         1471  +						uid = tonumber(words[2]) or math.random(0,0xffffFFFF);
  1427   1472   						members={};
  1428   1473   					}
  1429   1474   					table.insert(relsets[words[2]].members, path)
  1430   1475   				elseif state >= 2 and c == 'd' then syn(1) state = 3
  1431   1476   					table.insert(W.defs, {
  1432   1477   						part = words[2];
  1433   1478   						branch = {};
  1434   1479   						means = {};
  1435   1480   						forms = {};
  1436   1481   						phrases = {};
         1482  +						rels = {};
  1437   1483   					})
  1438   1484   					path = {w = path.w, dn = #(W.defs)}
  1439   1485   				elseif state >= 3 and c == 'dr' then syn(1)
  1440   1486   					table.insert(D.branch, words[2])
  1441   1487   				elseif state >= 3 and c == 'df' then syn(2)
  1442   1488   					if not inflmap[words[2]] then
  1443   1489   						id10t('no inflection form %s defined', words[2])
  1444   1490   					end
  1445   1491   					D.forms[inflmap[words[2]]] = words[3]
         1492  +				elseif state >= 3 and c == 'p' then syn(1) state = 14
         1493  +					table.insert(D.phrases, {
         1494  +						str = words[2];
         1495  +						means = {};
         1496  +						rels = {};
         1497  +					})
         1498  +					path = {w = path.w, dn = path.dn, pn = #(D.phrases)}
  1446   1499   				elseif state >= 3 and c == 'm' then syn(1) state = 4
  1447   1500   					table.insert(D.means, {
  1448   1501   						lit = words[2];
  1449   1502   						notes = {};
  1450   1503   						examples = {};
         1504  +						rels = {};
  1451   1505   					});
  1452         -					path = {w = path.w, dn = path.dn, mn = #(D.means)}
         1506  +					path = {w = path.w, dn = path.dn, pn=path.pn, mn = #(D.means)}
  1453   1507   				elseif state >= 4 and c == 'n' then syn(1) state = 5
  1454   1508   					table.insert(M.notes, {kind=words[2], paras={}})
  1455         -					path = {w = path.w, dn = path.dn, mn = path.mn, nn = #(M.notes)};
         1509  +					path = {w = path.w, dn = path.dn, pn = path.pn, mn = path.mn, nn = #(M.notes)};
  1456   1510   				elseif state >= 5 and c == 'np' then syn(1)
  1457   1511   					table.insert(N.paras, words[2])
  1458   1512   				end
  1459   1513   				-- we ignore invalid ctls, for sake of forward-compat
  1460   1514   			end
  1461   1515   		end
  1462   1516   	end
................................................................................
  1463   1517   
  1464   1518   	for k,v in pairs(relsets) do
  1465   1519   		if not v.uid then
  1466   1520   			--handle non-numeric export ids
  1467   1521   			v.uid = math.random(0,0xffffFFFF)
  1468   1522   		end
  1469   1523   		table.insert(new.relsets, v)
         1524  +
         1525  +		for q,m in pairs(v.members) do
         1526  +			table.insert(pathRef({dict=new},m).rels, v.uid)
         1527  +		end
  1470   1528   	end
  1471   1529   
  1472   1530   	local ofd = safeopen(ctx.file,"w+b")
  1473   1531   	local o = writeDict(new);
  1474   1532   	ofd:write(o)
  1475   1533   	ofd:close()
  1476   1534   end
................................................................................
  1507   1565   		if ctx.flags.human and lvl > 0 then
  1508   1566   			pfx = string.rep('\t', lvl)
  1509   1567   		end
  1510   1568   		ofd:write(pfx..string.format(...)..'\n')
  1511   1569   	end
  1512   1570   	local d = ctx.dict
  1513   1571   	o(0,'pv0 %s %s', san(d.header.lang), san(d.header.meta))
  1514         -	local function checksyn(obj)
  1515         --- 		for _,s in ipairs(d.synonyms) do
  1516         -		local lvl = 0
  1517         -		if     obj.nn then lvl = 4
  1518         -		elseif obj.mn then lvl = 3
  1519         -		elseif obj.dn then lvl = 2
  1520         -		elseif obj.w  then lvl = 1 end
  1521         -		if not d._relCache[obj.w] then return end
  1522         -		for _,s in ipairs(d._relCache[obj.w]) do
  1523         -			for _,sm in ipairs(s.members) do
  1524         -				if pathMatch(obj, sm) then
  1525         -					o(lvl,'r %u',s.uid)
  1526         -					break
  1527         -				end
  1528         -			end
         1572  +	local function checksyn(obj,lvl)
         1573  +		for k,v in pairs(obj.rels) do
         1574  +			o(lvl,'r %u',s.uid)
  1529   1575   		end
  1530   1576   	end
  1531   1577   	for i,f in pairs(d.header.inflectionForms) do
  1532   1578   		o(0,'f %s %s %s', san(f.name), san(f.abbrev), san(f.desc))
  1533   1579   		for j,p in pairs(f.parts) do
  1534   1580   			o(1,'fp %s', san(p))
  1535   1581   		end
         1582  +	end
         1583  +	local function scanMeans(tbl,path,lvl)
         1584  +		for j,m in ipairs(def.means) do
         1585  +			o(lvl,'m %s', san(m.lit))
         1586  +			local lp = copy(path)
         1587  +			lp.mn = j
         1588  +			checksyn(m,lp,lvl+1)
         1589  +			for k,n in ipairs(m.notes) do
         1590  +				o(lvl+1,'n %s', san(n.kind))
         1591  +				for a,p in ipairs(n.paras) do
         1592  +					o(lvl+2,'np %s', san(p))
         1593  +				end
         1594  +			end
         1595  +		end
  1536   1596   	end
  1537   1597   	for lit, w in pairs(d.words) do
  1538   1598   		o(0,'w %s',san(lit))
  1539         -		checksyn{w=lit}
         1599  +		checksyn(w,{w=lit},1)
  1540   1600   		for i,def in ipairs(w.defs) do
  1541   1601   			o(1,'d %s',san(def.part))
  1542         -			checksyn{w=lit,dn=i}
         1602  +			checksyn(def,{w=lit,dn=i},2)
  1543   1603   			for j,r in ipairs(def.branch) do
  1544   1604   				o(2,'dr %s',san(r))
  1545   1605   			end
  1546         -			for j,m in ipairs(def.means) do
  1547         -				o(2,'m %s', san(m.lit))
  1548         -				checksyn{w=lit,dn=i,mn=j}
  1549         -				for k,n in ipairs(m.notes) do
  1550         -					o(3,'n %s', san(n.kind))
  1551         -					for a,p in ipairs(n.paras) do
  1552         -						o(4,'np %s', san(p))
  1553         -					end
  1554         -				end
         1606  +			for j,p in ipairs(def.phrases) do
         1607  +				o(2,'p %s',san(p.str))
         1608  +				scanMeans(p.means, {w=lit,dn=i,pn=j}, 3)
  1555   1609   			end
         1610  +			scanMeans(def.means, {w=lit,dn=i}, 2)
  1556   1611   		end
  1557   1612   	end
  1558   1613   	for _,s in ipairs(d.relsets) do o(0,'s %s %u', s.kind, s.uid) end
  1559   1614   end
  1560   1615   
  1561   1616   local function filterD(lst, fn)
  1562   1617   -- cheap algorithm to destructively filter a list
................................................................................
  1569   1624   			top = top - 1
  1570   1625   		end
  1571   1626   	end
  1572   1627   	return lst
  1573   1628   end
  1574   1629   
  1575   1630   function cmds.mod.exec(ctx, orig, oper, dest, ...)
  1576         -	if (not orig) or not oper then
  1577         -		id10t '`mod` requires at least an origin and an operation'
  1578         -	end
  1579         -	local op, dp = pathParse(orig)
  1580         -	local w,d,m,n = safeNavWord(ctx, op.w,op.dn,op.mn,op.nn)
  1581         -	-- unfortunately, "pointers" exist elsewhere in the
  1582         -	-- structure, currently just from relsets, that must
  1583         -	-- be updated whenever an object moves or changes.
  1584         -	-- this is miserable and takes a lot of work, using
  1585         -	-- algorithms provided by the following functions.
  1586         -	-- note that we don't bother trying to update the
  1587         -	-- relcache as we go, it's simply not worth the work;
  1588         -	-- instead we simply rebuild the whole cache when
  1589         -	-- this command returns
  1590         -	local function cleanupRels(path, fn)
  1591         -		local rc = ctx.dict._relCache[path.w]
  1592         -		if rc then
  1593         -			for k,s in pairs(rc) do fn(s,k) end
  1594         -		end
  1595         -	end
  1596         -	local function cleanupRelsEach(path, fn)
  1597         -		cleanupRels(path, function(s,k)
  1598         -			local top = #s.members
  1599         -			for i=1,top do local m=s.members[i]
  1600         -				if pathSub(path, m) then
  1601         -					local val = fn(m,s,i)
  1602         -					if val ~= nil then
  1603         -						s.members[i] = val
  1604         -					end
  1605         -				end
  1606         -			end
  1607         -		end)
  1608         -	end
  1609         -	local function deleteRefsTo(path)
  1610         -		cleanupRels(path, function(s)
  1611         -		-- antonyms: delete the headword and transform the group
  1612         -		-- to a list of synonyms
  1613         -			if s.kind == 'ant' and pathSub(path,s.members[1]) then
  1614         -				s.kind = 'syn'
  1615         -			end
  1616         -			filterD(s.members, function(m)
  1617         -				return not pathSub(path, m)
  1618         -			end)
  1619         -		end)
  1620         -		if not path.dn then
  1621         -			ctx.dict._relCache[path.w] = nil
  1622         -		end
  1623         -	end
  1624         -	local function moveRelTree(op,dp)
  1625         -		cleanupRelsEach(op, function(old,set,idx)
  1626         -			local new = {}
  1627         -			for _,elt in pairs{'w','dn','mn','nn'} do
  1628         -				if dp[elt] ~= nil then
  1629         -					new[elt] = dp[elt]
  1630         -				else
  1631         -					new[elt] = op[elt] or old[elt]
  1632         -				end
  1633         -			end
  1634         -			return new
  1635         -		end)
  1636         -	end
  1637         -	local function shiftRelTree(dp, fld, nid, amt)
  1638         -		local cleanupTargetMask = ({
  1639         -			dn = {w=dp.w};
  1640         -			mn = {w=dp.w,dn=dp.dn};
  1641         -			nn = {w=dp.w,dn=dp.dn,mn=dp.mn};
  1642         -		})[fld] -- >____<
  1643         -		cleanupRelsEach(cleanupTargetMask, function(old,set,i)
  1644         -			if old[fld] >= nid then
  1645         -				old[fld] = old[fld] + amt
  1646         -			end
  1647         -		end)
  1648         -	end
  1649         -	local function insertAndMoveRelTree(tbl,n,op,dp,fld)
  1650         -		local nid = #tbl
  1651         -		local path = copy(dp)
  1652         -		path[fld] = nid
  1653         -		tbl[nid] = n
  1654         -		shiftRelTree(dp,fld,1)
  1655         -		moveRelTree(op, path)
  1656         -	end
  1657         -	if oper == 'drop' then
  1658         -		-- clean out the cache and delete relationships
  1659         -		deleteRefsTo(op)
  1660         -		if not d then
  1661         -			ctx.dict.words[op.w] = nil
  1662         -		elseif not m then
  1663         -			table.remove(w.defs, op.dn)
  1664         -		elseif not n then
  1665         -			table.remove(d.means, op.mn)
  1666         -		else
  1667         -			table.remove(m.notes, op.nn)
  1668         -		end
  1669         -	elseif oper == 'out' then
  1670         -		if n or not m then id10t '`mod out` must target a meaning' end
  1671         -		if not dest then id10t '`mod out` requires at least a part of speech' end
  1672         -		local newdef = {
  1673         -			part = dest;
  1674         -			branch = {...};
  1675         -			forms = {};
  1676         -			means = {m};
  1677         -		}
  1678         -		shiftRelTree(op, 'dn', op.dn, 1)
  1679         -		table.insert(w.defs,op.dn+1, newdef)
  1680         -		moveRelTree(op,{w=op.w, dn=op.dn+1, mn=1})
  1681         -		table.remove(d.means,op.mn)
  1682         -	elseif oper == 'move' or oper == 'merge' or oper == 'clobber' then
  1683         -		if dest
  1684         -			then dp = pathParse(dest)
  1685         -			else id10t('`mod %s` requires a target',oper)
  1686         -		end
  1687         -		if n then
  1688         -			if not dp.mn then
  1689         -				id10t '`mod` on a note requires a note or meaning destination'
  1690         -			end
  1691         -			local _,_,dm = safeNavWord(ctx, dp.w,dp.dn,dp.mn)
  1692         -			if dp.nn then
  1693         -				if oper == 'move' then
  1694         -					shiftRelTree(dp, 'nn', dp.nn, 1)
  1695         -					table.insert(dm.notes, dp.nn, n)
  1696         -				elseif oper == 'merge' then
  1697         -					local top = #(dm.notes[dp.nn].paras)
  1698         -					for i, v in ipairs(n.paras) do
  1699         -						dm.notes[dp.nn].paras[i+top] = v
  1700         -					end
  1701         -				elseif oper == 'clobber' then
  1702         -					deleteRefsTo(dp)
  1703         -					dm.notes[dp.nn] = n
  1704         -				end
  1705         -				moveRelTree(op,dp)
  1706         -			else
  1707         -				if oper ~= 'move' then
  1708         -					id10t('`mod note %s` requires a note target', oper)
  1709         -				end
  1710         -				insertAndMoveRelTree(dm.notes,n,op,dp,'nn')
  1711         -			end
  1712         -			if oper == 'move' and dp.nn and dm == m and op.nn > dp.nn then
  1713         -				table.remove(m.notes,op.nn+1)
  1714         -			else
  1715         -				table.remove(m.notes,op.nn)
  1716         -			end
  1717         -		elseif m then
  1718         -			if not dp.dn then
  1719         -				local newdef = {
  1720         -					part = d.part;
  1721         -					branch = copy(d.branch);
  1722         -					forms = copy(d.forms);
  1723         -					means = {m};
  1724         -				}
  1725         -				local didx
  1726         -				if ctx.dict.words[dp.w] then
  1727         -					local defst = ctx.dict.words[dp.w].defs
  1728         -					didx = #defst
  1729         -					defst[didx] = newdef
  1730         -				else
  1731         -					ctx.dict.words[dp.w] = {
  1732         -						defs = {newdef};
  1733         -					}
  1734         -					didx = 1
  1735         -				end
  1736         -				cleanupRelsEach(op, function(oldpath,set,mi)
  1737         -					return {w=dp.w,dn=didx,mn=1,nn=oldpath.nn}
  1738         -				end)
  1739         -				table.remove(d.means,dp.mn)
  1740         -			else
  1741         -				local dw, dd = safeNavWord(ctx, dp.w, dp.dn)
  1742         -				if dp.mn then
  1743         -					if dd.means[dp.mn] and (oper == 'merge' or oper=='clobber') then
  1744         -						if oper == 'merge' then
  1745         -							dd.means[dp.mn] = dd.means[dp.mn] .. '; ' .. m
  1746         -						elseif oper == 'clobber' then
  1747         -							deleteRefsTo(dp)
  1748         -							dd.means[dp.mn] = m
  1749         -						end
  1750         -					else
  1751         -						cleanupRelsEach({w=dp.w,dn=dp.dn}, function(old,set,i)
  1752         -							if old.mn >= dp.mn then
  1753         -								old.mn = old.mn + 1
  1754         -							end
  1755         -						end)
  1756         -						table.insert(dd.means, dp.mn, m)
  1757         -					end
  1758         -					moveRelTree(op,dp)
  1759         -				else
  1760         -					insertAndMoveRelTree(dd.means,m, op,dp,'mn')
  1761         --- 					table.insert(dd.means, m)
  1762         -				end
  1763         -				if oper == 'move' and dp.mn and dd.means == d.means and op.mn > dp.mn then
  1764         -					table.remove(d.means,op.mn+1)
  1765         -				else
  1766         -					table.remove(d.means,op.mn)
  1767         -				end
  1768         -			end
  1769         -		elseif d then
  1770         -			local ddefs = safeNavWord(ctx, dp.w).defs
  1771         -			if dp.dn then
  1772         -				if oper == 'merge' then
  1773         -					local top = #(ddefs[dp.dn].means)
  1774         -					for i,om in ipairs(d.means) do
  1775         -						ddefs[dp.dn].means[top+i] = om
  1776         -					end
  1777         -					for k,p in pairs(d.forms) do
  1778         -						deleteRefsTo(dp)
  1779         -						ddefs[dp.dn].forms[k] = p -- clobbers!
  1780         -					end
  1781         -				else
  1782         -					shiftRelTree(dp,'dn',dp.dn,1)
  1783         -					table.insert(ddefs, dp.dn, d)
  1784         -				end
  1785         -				moveRelTree(op,dp)
  1786         -			else
  1787         -				insertAndMoveRelTree(ddefs,d, op,dp,'dn')
  1788         --- 				table.insert(ddefs, d)
  1789         -			end
  1790         -			if oper == 'move' and dp.mn and w.defs == ddefs and op.mn > dp.mn then
  1791         -				table.remove(w.defs,op.dn+1)
  1792         -			else
  1793         -				table.remove(w.defs,op.dn)
  1794         -			end
  1795         -		else
  1796         -			if ctx.dict.words[dp.w] then
  1797         -				if oper ~= 'merge' then
  1798         -					id10t('the word “%s” already exists; use `merge` if you want to merge the words together', dp.w)
  1799         -				end
  1800         -				for i,def in ipairs(w.defs) do
  1801         -					local odp = copy(op) odp.dn = i
  1802         -					local ddp = {w=dp.w, dn=dp.dn+i-1}
  1803         -					if dp.dn then
  1804         -						shiftRelTree(dp, 'dn', dp.dn+i-1, 1)
  1805         -						table.insert(ctx.dict.words[dp.w].defs, dp.dn+i-1, def)
  1806         -						moveRelTree(odp,ddp)
  1807         -					else
  1808         --- 						table.insert(ctx.dict.words[dp.w].defs, def)
  1809         -						insertAndMoveRelTree(ctx.dict.words[dp.w].defs, def,
  1810         -													odp,dp,'dn')
  1811         -					end
  1812         -				end
  1813         -			else
  1814         -				ctx.dict.words[dp.w] = w
  1815         -				moveRelTree(op,dp)
  1816         --- 				ctx.dict._relCache[dp.w] = ctx.dict._relCache[op.w]
  1817         --- 				ctx.dict._relCache[op.w] = nil
  1818         -			end
  1819         -			ctx.dict.words[op.w] = nil
  1820         -		end
  1821         -	end
  1822   1631   	rebuildRelationCache(ctx.dict)
  1823   1632   end
  1824   1633   
  1825   1634   local function fileLegible(file)
  1826   1635   	-- check if we can access the file
  1827   1636   	local fd = io.open(file,"rb")
  1828   1637   	local ret = false