读SIM源码笔记 -- 1

背景

人生第一次读项目源码。

这是SIM查重工具,由大牛Dick Grune创造。文档和源码都可以在他的网站里找到。Paper是.ps文件,需要下载Adobe Acrobat打开阅读,也可以用Acrobat把它导出成pdf,用习惯的pdf阅读器看。

关于怎么运行他的源码,我的上一篇博客记录了配置MinGW的过程。当然,只是使用的话,直接下exe包就行了。

前段时间云里雾里地读了一下SIM提供的三个文档,Manual,Paper,Technique Report,然后这几天一头扎进源码里面,希望细节性地了解了它的实现以后,试图做一点创新,以此作为我大创项目的主要工作(这都三月份了,大创四月份就要结题了,说实话慌得很)。

第一次读源码,没有经验,就一头扎到main里面一层层地读下去了,过程中有一些心得随缘地记录一下。

 

前言

光对着源码看几秒钟就走神,所以一边写一边看来让自己注意力集中,这就是一篇笔记,质量就不强求了。

 

因为是一头扎到main()里面去看的,所以就从main讲起吧。

初看sim.c

main()在sim.c里面,所以我就从sim.c开始看起了。有一个感受,就是开始写这篇博客的时候看过的函数已经遍布五六七八个文件了,但是每个文件的代码量其实都在差不多的范围里,差不多都是200到400行的样子,这里面是有不少封装的思想。

oplist[]

这个文件里main()之前的内容并不多,最显眼的还是那一长串的oplist,这个是选项列表,因为sim工具给用户提供了许多的设置选项,可以根据不同需要进行查重,比如哪些文件不需要相互进行比较(old文件)、要不要按百分比显示查重率、百分之多少以上的重复率输出等等。

这个指令列表预设了这些指令,它是stuct option*类型,关于这个结构体,需要注意的就是里面有一个enum类型的变量,它指示的是这个指令后面所跟的参数,比如-r后面需要跟一个整型参数,表示MinRunSize被调整成多少。

读入参数指令

在Read_Input_Files(argc, argv)被调用之前,其实都是一些准备的工作,比如把版本号设置出来,保留progname,读入从命令行得到的参数,调用do_options,把这些参数都存到options列表当中去,用于后面检查哪些参数是被设定的。可以说主要就是对输入的参数指令进行检查,再做一些处理,比如如果接收到-v这个参数,就可以直接输出版本号结束程序了。

这里面应该说,感受到真正的项目和我之前做的算法题,还有我做的一些大作业比起来,它的出错处理就做得很精细了,在这里的准备工作里面,就对各种命令之间的冲突做了判断,如果有所错误,会进行相应的报错,然后退出。

读入文件

主要是Read_Input_Files的调用。

其实就是这一块略略读完之后,激发了我写博客的动力。之前也就碰到个结构体,是option,到这里它开始了层层调用,并且出现了text这个结构体,我怕不写一下博客,过几天回来看就忘了这一块在干什么了。

/*这是text类型的定义*/ 
struct text {
	const char *tx_fname;	/* the file name */
	size_t tx_start;	/* index of first token in Token_Array[]
				   belonging to the text */
	size_t tx_limit;	/* index of first position in Token_Array[]
				   not belonging to the text */
	/* 就表示这一段在Token_Array里面是[tx_start, tx_limit) */
	int tx_EOL_terminated;	/* Boolean */ /*暂时不知道干嘛的*/
	struct position *tx_pos;/* list of positions in this file that are
				   part of a chunk; sorted and updated by
				   Pass 2
				*/
};

struct position {
	/* position of first and last token of a chunk */
	struct position *ps_next;
	int ps_type;		/* first = 0, last = 1, for debugging */
	size_t ps_tk_cnt;	/* in tokens; set by add_run()
				   in Read_Input_Files() */  /*最后用来输出匹配数量*/
	size_t ps_nl_cnt;	/* same, in line numbers;set by Retrieve_Runs(),
				   used by Print_Runs(), to report line numbers
				*/  /* 最后用来输出匹配行号 */
};

 其实感觉text已经像是被封装成一个类了,不过只是封装的感受强烈,OOP当中多态和继承的思想它没有用到(也不需要吧)。目前对text的理解,就是它其实是指示了,在Token_Array当中,当前这个文件从第几号Token开始,从第几号Token结果。position的属性要等到pass2再来详看作用了。

Read_Input_File的调用一层又一层,尤其是“打开文件”这一个功能,后面还有流式文件的封装,给我绕晕了。琢磨了一段时间发现没啥效果,我就“抓大放小”地把后面几层调用先给放了。总的来说,这个模块就是把打开的文件中的token识别出来,存放在Token_Array数组当中。追溯获得token的方法,就是通过flex的yylex()来获得。词法分析和token的存储还蛮重要的,第一遍看没有能特别看得细,后面可能需要回来细看。

Compare_Files()

main()当中对这个函数的注释是"turns texts into runs",我在看文档的时候就对run这个单位有些迷惑,这部分明显是真正开始核心工作的地方。

这个函数又由三个函数嵌套形成:

void
Compare_Files(void) {
	Make_Forward_References();
	compare_texts();
	Free_Forward_References();
}

Make_Forward_References()

从TechnReport里大概了解到Foward_References,它是加速匹配的一种方法,但是看的时候对使用last_index[]对Foward_References进行更新的过程有所迷惑。这里方便起见,翻译一下TechnReport,也是注释里,对这个过程的描述。

文件的比较方法是,每个子串都和它所有右边的子串进行匹配,这个过程实际上是平方的复杂度,但是由于我们只会去关注超过'Min_Run_Size'长度字串的匹配结果,这就是我们有可能使用哈希表进行优化。

对文本中的每个位置p,我们构造一个下标数组,forward_reference[p]指示下一个长度为Min_Run_Size的hash值相同的run开始的位置,哈希值由hash1()计算得到。如果找不到这样的run,指向的下标就是0。

为了构造这个数组,我们使用一个哈希列表latest_index[],它的长度是一个素数,并且长度与text数组长度相仿。这个哈希列表latest_index[]满足,latest_index[i]是最近出现的哈希值为i的token的位置,如果没有这样的token,值为0。可以查看Make_Forward_References()。

由于哈希的偶然性,上述的构造forward references的方法并不完美。第二遍的扫描(make_forward_references_perfect())通过对Min_Run_Size的tokens做一个完全比较的方法让它们变得完美。(对the LaTeX sources of our book “Modern Compiler Design, 2nd Ed”.)这将总链长度从103555减少到345,这由db_forward_reference_check()测定。db_forward_reference_check()测定数据,检测forward reference。

看完这里的文档,我有一个疑问,文档描述中似乎将token与run进行哈希值的比较,而按照我的理解,run应该是指长度为Min_Run_Size的tokens。我还有一个对“不完美的”理解,就是按照第一遍的哈希结果,应该有很多并不相似的run被判成了相似,所以带着这个疑问往下看。

BTW,在看init_hash_table()的时候,看到TryCalloc函数,百度未果,才发现源码里有Malloc.h,所以他老人家还专门设计了空间管理?这就很厉害了,不过我没有去深究,大概知道它在分配空间就行了。

按照上面所说,扫描实质上是做了两遍的,也就是正好对应:

make_forward_references_using_hash() 

make_forward_references_perfect()

第二遍可以理解成是在第一遍基础上的一种“加强”。我们挨个来分析。首先是make_forward_references_using_hash()

先按照上述所说的,为last_index[]分配好尽量和token数相仿长度的素数长度的空间。

申请完之后,经历一个核心的两重循环后,释放last_index[]的空间。这部分最核心的就是这个中间这个两重循环。可能是学习状态一直不好,看这个两重循环我屡败屡战,每次都问自己,”小问号,你是否有很多朋友?“

外层循环是在遍历所有文件,并在每一次进入内层循环之前把hash值设为0(这个设为0引起了我的注意,因为我本来以为在compare的过程中,token1和token2是否在同一个text里不会影响到他们匹配的,但是hash值却在进入不同的文件之后进行一次初始化操作。这么做的真正用处后面再分析)。在进入内层循环之前,还有两个宏定义,一个是使用宏定义定义了一个函数(眼花缭乱的操作,我做算法题的时候几乎从来不用宏定义撸函数,虽然知道可以这么做),它的作用是以32位规模,进行循环左移。还定义了一个常量SHIFT为5。

进入内层循环,用j作下标来遍历当前文件的token。这层循环比较关键,贴一下代码:

                    if (	
                        j - txt->tx_start >= Min_Run_Size
                    ) {	
                        int oldest_value =
                        Token2int(Token_Array[j - Min_Run_Size]);
			int oldest_shift =
				   ((Min_Run_Size-1) * SHIFT) % 32;  /* oldest_shift这样写,循环过程中都不会有变化吧。 */
			hash ^=
				Left_Circular_32(
				    oldest_value, oldest_shift
				);
			}
			/* Circular left shift */
			hash = Left_Circular_32(hash, SHIFT);	
			/* Add new token */
			hash ^= Token2int(Token_Array[j]);

			if (j - txt->tx_start < (Min_Run_Size - 1)) {
				/* no */
				continue;
			}

			size_t run_start = j - (Min_Run_Size - 1);

			/* Can the run be useful? */
			if (!May_Be_Start_Of_Run(Token_Array[run_start]))
				continue;			/* no*/

			/* the hash value is used here for an index */
			size_t h = hash % latest_index_table_size;

			if (latest_index[h]) {
				forward_reference[latest_index[h]] = run_start;
			}
			/*latest_index[h] = j;*/
			latest_index[h] = run_start;

为了看起来不显得很长,我删了不少注释,需要看作者注释的话可以下载文章开头的源码。说起来这份源码是真的注释丰富,这个习惯也很值得学习啊。这一部分的作用就是进行第一遍hash,按照上述TechnReport文档所说,使用last_index[]为媒介,将具有相同哈希值的tokens串(每个串长度为Min_Run_Size)通过forward_references[]连接起来,现在通过forward_references[],其实可以找到一串串链表,串联所有具有相同哈希值的tokens串,每一串大小为Min_Run_Size。这样就解决了前面看完文档的疑问,当然是相同长度的token串在进行比较啦。

看文档的意思,这样的结果可能会串联过多的串,所以进行第二遍哈希,希望打破一些实际上无意义的串。

第二遍的扫描,就是make_forward_references_perfect()了。这里就遍历了Token_Array[]中的每一个token,对于每一个token的位置i,j初始化成i,然后不断地进行j = forward_references[j],直到i位置和j位置的min_run完全相等,这时候将forward_references[i]赋值为j,这样就直接链接到了j的位置,相当于砍掉了中间一串并不符合要求的链接。直接进行这一遍的扫描而不顾之前的哈希,其实也可以达到“完美化forward_references[]"的目的,但是那样的复杂度就是n方了,而第一遍扫描的复杂度只有n,使用n复杂度的扫描,其实是将哈希值相同作为一个”成功链接的必要条件“,筛掉了很大一部分无意义的匹配,这样来极大地降低了第二遍扫描的成本。

通过这两遍的扫描,Make_Forward_References[]就结束了。

compare_texts()

compare_texts()的总体顺序是:

对于所有的文件

        对于所有它必须比较的文本

                对于所有在新文件中的位置

                       对于所有在文本中的位置

                               对于每一个增长的规模

                                       尽量匹配,并维持最佳

这里有一个新的数据结构,struct range,包括三个数,起始位置、终止位置,第三个sticky暂时没明白。

上来就用n遍历所有文件。下面讨论遍历的过程。

先定义range,然后赋一些默认值。关于代码,这里面其实,像beginning_of_text是可以放到循环体外面的,因为它是Text[0].tx_start,这里可能是为了美观或者方便阅读?range.rg_start = Text[n].tx_start + 1,我不太明白为什么要加一。以及sticky赋值为0,也不是太明白。

接下来三个if就是根据设定的指令来调整了,如果设定了'a'指令,则把range.rg_limit设为Text[n].tx_start(原先是end_of_text,即所有文件的末尾),这样设置的含义就是和全体文件比较,可能是从rg_start开始比较到end_of_text之后,还会返回到开头,继续比较到rg_limit为止。sticky还是不知道啥意思。对于'S'指令,就是只比较old文件了,所以rg_start就是在beginning_of_old_text。设定了's'指令,就是不与自己比较,rg_start等于Text[n].start + 1时,调整为Text[n].limit。不等于呢?那就是设定了'S'的情况,这时候就不改了。

然后检查一下range是不是空的。比如-sS时,n指向最后一个new文件,那么range到这里会被置成rg_start == rg_limit。

若设定'e'需要逐个文件比较,这里就涉及compare_one_on_one()和compare_one_text(),'e'用前者,无'e'用后者。

compare_one_on_one(int n, int m, struct range *rg)

先检测m文件是否在rg内,txt1指向m。构造一个range_m表示m文件的范围。然后再进行compare_one_text(n, &range_m)(所以还是要通过compare_one_text)。

compare_one_text(int n, struct range *rg)

看函数名和参数表,还是容易猜的,这个大概就是要把n号文件和rg范围内的内容进行比较了。不过关注点肯定还是比较具体要怎么进行,如何运用到前面的forward_references[]。

创建txt0指针指向n号文件,i0为txt0->tx_start。完成这两部后,看到了一个宏定义的代码块,叫作#if_def DB_COMP,大概是作者调试用的语句,之后计划在这份源码基础上做一些改进的,到时候可以尝试利用这些指令。当然,记得#if_def要跟#endif。

然后是一个循环,条件为i0 + Min_Run_Size <= txt0 -> tx_limit,按照注释来说,这是“有一个足够的运行空间”的前提。

循环内先判断Token_Array[i0]可以作为运行的开始,用了May_be_Start_Of_Run(),这个函数返回的是!is_in_set(non_initials, tk),is_in_set()在!is_regular_token(tk)的时候返回0,这部分涉及到regular_token的问题,这个是我还没看懂的Token处理的开始部分,就先略过,学一下比较的方法以后再回头研究。

所以如果它满足作为Start_Of_Run的条件,我们还要看看是不是真的存在一个run。先是定义txt_run,i_run,run_size,判定的条件为run_size。run_size = lcs(txt0, i0, rg, &txt_run, &i_run)。lcs,这就有熟悉的味道了。如果run_size大于0,将会add_run(txt0, i0, txt_run, i_run, run_size),i0 += run_size。等于0,那么就只能i0++去看下一个token了。

函数的末尾还有判断rg的sticky值的操作,满足的话,rg->rg_start = i0 - 1,这就相当于rg_start跟着i0一起往前走。这样做的用处,就要分析完lcs()才知道了。

static size_t lcs(struct text *txt0, size_t i0, struct range *rg, struct text **tx_bp, size_t *i_bp)

这是源码里看到现在为止最长的一个函数,突破200行。读着读着有了个心得,叫作注释一定好好看。虽然注释是英文的,但是肯定比代码好懂,毕竟注释是人类的语言,代码是机器的语言。能和一个人交流,就不要选个和一堆代码硬刚。不过以前刷博客的时候,看到过有注释和代码不匹配的情况,这个就另当别论了,出现这种情况分分钟搞崩心态。

这个函数,参数表有注释。

txt0,用来比较的文本。i0,txt0中的起始位置。rg,搜索范围。接下来是两个输出参数,看看是否返回值大于0。tx_bp,输出,best run的文本。i_bp,在best run的文本中的起始位置。

找到最长公共子串(不是最长公共子序列),在如下范围中寻找:从i0位置开始的txt0,对比rg范围中的所有文本。在tx_bp和i_bp中写下位置,并返回size。如果公共子串没有找到,返回0。

检查i0是否在txt的范围中,若不是则fatal。从前面调用lcs的环境来看,是不会出现这种情况的,不过为了保证lcs作为一个模块,自身具有健壮性,这里进行错误处理也是很必要的。这里之后就是调试语句,这一遍看就不分析调试语句了。

来到一个for循环,i1为迭代的量,初始值为first_forward_ref_for(i0, rg)。

first_forward_ref_for()是做啥的呢,它不停地在寻找Forward_Reference(res, i0)。

Forward_Reference(i, i0)又是做啥的呢(就是这样一步步调用下去,相当折腾人。其实也看到过说,人脑能构造栈的深度蛮小的,我这种菜鸟,深度更小了,超过2层就迷糊。但是单独去读每一个文件更加不太能接受,所以还是要硬刚),看到,i为0或者不小于n_forward_references的时候报错,这就是不允许i为forward_references[]的边界。然后new_i即为i的前向链接(为了叙述把forward_references直译了,实际上是从每个点向后串联的)。res作为返回值,主要就是检测new_i是否为0或i0,再赋给res。new_i为i0的情况,res也赋值为0,此时判定为循环。所以i0就是寻找前向链接的终点。总结起来,Forward_Reference就是“寻找i的前向链接,同时判定错误、边界情况”这样的功能。

这样再回来看first_forward_ref_for(),它的功能就是寻找第一个在rg范围内的,i0的前向链接。

这样再回来看这个for循环的迭代过程,就是让i1在rg范围内不断更新为下一个i0的前向链接的值(啊,这样写思路的过程,一层层的函数入栈出栈,脑子真像一个工作栈)。

循环里,首先二分找到i1指向位置所属的文件txt1,检测一下i1和txt1是否匹配,然后就开始寻找better_size。根据size_best是否更新过,赋成size_best + 1,或者Min_Run_Size,这个就是,如果size_best还未赋值,则初始就从Min_Run_Size开始,如果已经赋值了,就把它扩大1,尝试能不能匹配成功更大的长度。

接下来这一块又很关键了(因为它很长)。j0,j1分别是i0,i1在better_size下的右端点。保证j0,j1不超过文件范围,且两段区间不重合的情况下继续尝试匹配。注意不重合不止是j0 < i1,也有可能是j1 < i0,当开了‘a'指令的时候。匹配的时候,区间中开始的Min_Run_Size长度不需要匹配,所以从后往前匹配better_size - Min_Run_Size长度的tokens即可。匹配通过之后,再尝试向后匹配,能不能将长度拉到更长。尝试完了,得到一个new_size,把它交给Best_Run_Size检查一下,可能会删掉一些语言上不接受的长度(评判的标准和细节后面再细看),然后把返回值更新一下,循环就了事了。

所以总结一下,lcs,就是对于txt,i0,找到能够和i0这个位置匹配的最大长度的tokens串,并返回这个长度,利用指针返回相应的位置*i_bp和文件*tx_bp。其实这个模块并不长的,只是充满了注释和调试语句。

这么看来,sticky是在'a'指令下,因为要进行所有文件的比较,所以比较范围需要跟着i0往前走,避免出现自己和自己比的情况。

找到每个位置的最佳匹配之后,就进行add_run()。本来说add_run可以先不看了,结果发现直接往后看Print部分有些云里雾里,明显这里不能够略过了。add_run()是分成两个部分来做的,分别是设定了'p'和未设定'p'的部分。

add_to_percentages(txt0, txt1, size) ---> do_add_to_percentages(0, txt0, txt1, size)

如果设置了指令'p',那么add_run()就会调用add_to_percentages()。

match_list是一个链表的表头,我们看到match的数据结构:

struct match {
    struct match *ma_next;
    const char *ma_fname0;
    const char *ma_fname1;
    size_t ma_size;			/* # tokens of file 0 found in file 1 */
    size_t ma_size0;		/* # tokens in file 0 */
};

在do_add_to_percentages()中,用m作为迭代变量,在match_list中寻找txt0和txt1的记录,如果找到了,对应的ma_size += size,然后退出。如果没有找到,就要创建新的节点插入。

创建新的节点就要申请空间。说起来我之前主要是做算法题,基本没考虑空间问题,这里是很细致地考虑空间的。他在空间不够的情况下,先把当前的链表里的都输出,再重新尝试分配一遍。参数里的rec_level就是检测是不是这种情况下的“第二次申请”,如果是,则内存不够退出了,不然一直申请下去一直不行就是死循环。

申请到空间之后,头插法插进match_list就没有问题了。

在分配空间的时候,如果空间不够,处理方法是把当前链表里所有的匹配按照百分比输出。刚开始我考虑这里会不会出现匹配遗漏的情况,比如file_a和file_c比较尚未完成,因为file_a和file_b的新匹配对申请空间而输出。不过其实不会,因为比较是按照文件顺序进行的,如果file_a和file_b进行比较,而链表里已经有file_a和file_c的记录,则说明file_a和file_c已经完成了匹配。也正是利用这一点,在函数末尾判断'u'指令存在的时候,直接输出链表中头结点后面的old匹配对,不会造成遗漏的情况,同时也是old对完成匹配的理想判断条件。

add_to_runs(txt0, i0, txt1, i1, size)

如果没有设置指令'p',add_run()的工作是调用add_to_runs()。

很显然,为了输出百分比和为了输出runs,存储run的时候用到了不一样的数据结构。

struct run {				/* a 'run' of coincident tokens */
	struct run *rn_next;
	struct chunk rn_chunk0;		/* chunk in left file */
	struct chunk rn_chunk1;		/* chunk in right file */
	size_t rn_size;
};

( 写这里的时候不知道为什么sublime里面复制出来,空格就会一个顶俩,后来知道了是因为编辑器里把tab作为了indentation,sublime可以在右下角Convert Indentation to Spaces,就可以恢复正常。)

add_to_runs()的思路很简单,按照上面run的数据结构,新建立一个节点r,把两个chunk按照两个txt和两个i分别设定好(set_chunk),然后设定rn_size,最后头插法把r插到runs里作为头节点。那么set_chunk(cnt, txt, start, size)的工作就是需要关注的了。

这时候再来关心chunk的结构:

struct chunk {
	/* a chunk of text */
	const struct text *ch_text;	/* pointer to the file */
	struct position ch_first;	/* first in chunk */
	struct position ch_last;	/* last in chunk */
};

 所以set_chunk同样地管理好这个结构体里面的值。position怎么管理呢?set_position(pos, type, txt, start)。不用说,这就要关心position的结构了:

struct position {
	/* position of first and last token of a chunk */
	struct position *ps_next;
	int ps_type;		/* first = 0, last = 1, for debugging */
	size_t ps_tk_cnt;	/* in tokens; set by add_run()
				   in Read_Input_Files() */  /*最后用来输出匹配数量*/
	size_t ps_nl_cnt;	/* same, in line numbers;set by Retrieve_Runs(),
				   used by Print_Runs(), to report line numbers
				*/  /* 最后用来输出匹配行号 */
};

 position指示文件中的一个位置,同时也是一个链表节点,这是每个text都会带有的一个链表,存放一个文件中所有构成chunk的position。根据set_position(),ps_tk_cnt是记录tokens的位置的。所以从每个position中可以读到一个token的位置,也可以按照它的指针读到下一个位置节点,按照set_position()的顺序来看,对于[a, b)这样一个区间,将会先读到b,再读到a,而ps_type为0是左端点,为1是右端点。当然,一个chunk中直接记录了左右端点,所以想要获取这个信息的话并不需要通过链表来搜。

把大脑里深入到position这一层的工作栈弹出position,可以考虑到chunk此时已经记录了匹配上的一个文件的一部分。再弹出chunk,可以考虑到run已经记录了两个匹配的文件各自的始末位置。

Free_Forward_References()

释放forward_reference的指针就可以了。这样,Compare_Files也就结束了,达到的效果就是将每个起始位置,都找到了最匹配的token串,变成run保存起来。而我的疑问就是,构造run的过程中,是保留了最大长度的匹配,那么会不会存在次大长度的匹配仍然是抄袭的,但是没有被检测出来的情况?

后来想了想,没有必要再考虑这个问题,对于当前文本来说,这一段被认定为抄袭,这就够了,不需要知道它抄袭的来源有多少。

Print_Percentages()  /*  设定了'p',用百分比输出 */

要开始分析输出模块了,其实输出模块结束,后面就需要回头思考词法分析部分了。

就Print_Percentage()这个函数而言,它首先进行sort_match_list(),然后print_match_list()。排序是为了使得对于同一个文件而言,百分比大的匹配在前面。这个排序,sort_match_list(&math_list),让我看到了从没见过的一个神奇用法。

sort_match_list()

在所有.c,.h文件里,关于sort_match_list()的定义只有这些:

static void sort_match_list(struct match **listhook);
#define	SORT_STRUCT		match
#define	SORT_NAME		sort_match_list
#define	SORT_BEFORE(p1,p2)	(match_percentage(p1) > match_percentage(p2))
#define	SORT_NEXT		ma_next
#include	"sortlist.bdy"

 然后排序就能完成了。意念排序?当然不是。这一块的最后一句include了一个.bdy文件,在源码目录找到这个文件,它实现了链表的归并排序。上述宏定义定义了几个名字之后,就可以调用了。这就相当于是一个模板了,神奇。

print_match_list()

在print_match_list()里面,也只有一个只有一行循环体的循环:

while (match_list) {
    print_and_remove_perc_info_for_top_file(&match_list);
}

所以其实入口在print_and_remove_per_info_top_file当中。它的功能就是每次输出一个文件的匹配结果。因为链表已经排序了,所以每次先输出头结点。'P'指令的作用是,控制每个文件只输出一个最大的抄袭,所以没有'P'的时候,先输出头结点,然后扫描整张链表,碰到文件名相同的,也输出,并且把这个节点从链表中断开。没有'P',同文件名的就不输出了。在输出每个文件的时候,用了指针的指针传参,理解的时候卡顿了一下,后来发现这样还是很巧妙的。不过作者在注释里表示这段sneaky,我还没有看出来是为啥:

static void
print_and_remove_perc_info_for_top_file(struct match **m_hook) {
	struct match *m = *m_hook;
	const char *fname = m->ma_fname0;

	print_perc_info(m);		/* always print main contributor */
	*m_hook = m->ma_next;
	Free(m);

	while ((m = *m_hook)) {
		if (m->ma_fname0 == fname) {
			/* print subsequent contributors only if not
			   suppressed by -P
			*/
			if (!is_set_option('P')) {
				print_perc_info(m);
			}
			/* remove the struct */
			*m_hook = m->ma_next; /* 这一步是直接把之前地址链接到下面去了 */
			Free(m); 
		} else {
			/* skip the struct */
			m_hook = &m->ma_next;
		}
	}
}

理解这两句话的时候愣了一会儿:


/* remove the struct */
*m_hook = m->ma_next; /* 这一步是直接把之前地址链接到下面去了 */

/* skip the struct */
m_hook = &m->ma_next;

其实是用m_hook不断地追踪下一个节点,而*m_hook就是在修改当前节点的地址了。倘若对这部分有疑问,不妨画个图,第一层是match,第二层是*match,第三层是**match,看看这两句话分别在改变什么东西。

Retrieve_Runs() /* 未设定'p' */

这一块就是在挨个地对每个文件pass2_txt。

在pass2_txt中,先检测文件的tx_pos是否为空,为空则无需扫描。然后打开这个文件。

对txt对应的tx_pos链表进行排序。为什么要排序呢?txt_pos链表是在add_to_runs()模块中,set_position的时候逐个设置的。对于一个text而言,被设置的position有来自“它前面的文件和它产生的匹配”,还有来自“它和它后面的文件产生的匹配”,这样产生的txt_position就是乱序的。用什么方法排序呢?果然,前面的"sortlist.bdy"文件再一次地用上了,把对应的变量名称宏定义一下,就可以直接用了,美丽的代码复用。

match_pos_list_of(txt)。现在我们已经获得了一个排序过的txt_pos链表,现在就是要为每一个pos找到对应的行号。采用的办法是,并行地扫描pos和file,找到合适的行号赋值给pos,再考虑下一个。

这里产生对TK_CNT_HORROR的疑问,在Open_Stream可以看到:

lex_tk_cnt = 0;		/* but is raised before the token is delivered,
				   so effectively *_tk_cnt starts at 1:
				   TK_CNT_HORROR
				*/

并不知晓为什么这个计数会在token给出之前增加,等回头看分析词法的时候再去考虑。

经过取回runs的操作,pos和行号就对应上了。

Print_Runs()

这块是真的凶,又是一个三百多行的.c文件。看pass3.c的文件,开头部分的注释讲到编码格式、字符粒度等等看起来非常陌生的东西,极大地阻碍了我读下去的信心和积极性,其实是为了尽量保证输出的格式。(看pass3.c文件的时候,发现按函数的定义顺序读可能更加容易一些,之前那样递归地读下去,一会儿就碰到新的陌生的模块,而顺序读,读着读着发现他用到的都是之前读过的模块。当然,这还是在源码严格按照“先定义后使用”的顺序写的前提下的。)

稍微看了一些,暂时不全部看细了。今天请教大佬学姐,她表示搞这些东西不能看的太细,研究和要改的部分的有关的内容,其他地方大概了解用途就好了,我之前有些偏执了,所以掉过头来抓紧看词法。词法就发在下一篇吧。

 

 

 

 

 

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值