
像许多年轻的开发人员一样,当希望找到工作/实习时,我朝着凉爽的IT公司发展。
最近,我试图加入JetBrains的行列,在削减预算的情况下,我准备分享我的经验。
为什么“几乎”成功?
当然,您立即会遇到这样的问题。
我认为,我的简历不错,并取得了很多成就和技能,过去八到九年间我一直在不断提高自己的水平。
我完成了测试任务(对我来说似乎很好),之前访问了位于我市的JB办公室,与HH和公司的一些开发商进行了交谈,结果被拒绝了实习而未加任何评论。
原因很可能是因为JetBrains专门选择学生进行实习,而现在我刚刚从11年级毕业,并且一次又一次地通过了考试。
好吧,这是整整一年让自己振作起来并申请下一年的机会。
测试任务分析
提交实习申请和测试测试作业的截止日期已经结束,这意味着包括我在内的所有解决方案的人都可以发布对这些作业的分析,以便明年任何感兴趣的学生都可以在开始实习之前了解大致的作业水平。他将不得不面对的问题,在这种情况下,他的知识将得以发展。
我向Corotin调试器开发团队申请了Kotlin的实习机会。
这个团队的实习期是在今年实习的这个团队中,最终确定调试器的这一部分及其与IDE的集成。
这项任务有些令人期待-为小型PL编写调试器。
我不会说这很复杂,相反。 它不需要任何有关构建翻译器理论的知识和很酷的技能。 但是,尽管如此,那些在这方面申请实习的人至少应该具备这些基本知识,并且可以毫无困难地应对这项任务。 当我决定在github上搜索我的“竞争对手”解决方案的关键字时,我感到惊讶,并在大约6-7个空存储库或几段代码中发现了1-2种或多或少有效的解决方案,之后人们放弃了。 也许我看上去很糟,但是结果仍然不令人满意。 如果放弃此任务的人可以阅读这篇文章-以后就不需要这样做。 在一个极端的情况下,在任务上坐上几天就足够了,我相信您会处理的。
任务本身的文字目标:为普通编程语言Guu实施分步代码执行。
注意:在以下描述中,有意省略了一些要点。 通常,它们由您自行决定。 如果完全无法理解,请写信给(这是我决定删除的邮件)。
Guu程序由一组过程组成。 每个过程都以子行(子名)开头,并以另一个过程的声明结尾(如果文件中的过程是最后一个,则以文件结尾)。 执行从sub main开始。
过程的主体是一组指令,每条指令位于单独的行上。 在行的开头可能会出现无关紧要的制表符或空格。 空行将被忽略。 没有关于Guu的评论。
Guu只有三个运算符:-设置(变量名)(新值)-为变量设置新的整数值。 -调用(子名称)-调用过程。 调用可以是递归的。 -打印(变量名)-在屏幕上打印变量的值。
Guu中的变量具有全局范围。 下面的程序将显示a = 2行。
次主
设置1
叫foo
打印一个
子富
设置一个2
这是具有无限递归的最简单的程序:
次主
致电主要
您需要为Guu编写逐步的解释器。 启动时,调试器应在sub main中的第一条指令所在的行上停止,并等待用户的命令。 所需的最少调试器命令集:
我-进入调试器,进入内部调用(子名称)。
o-单步执行,调试器不会进入调用内部。
trace-从主行开始以行号打印堆栈跟踪执行...
var-显示所有已声明变量的值。
用户与调试器的通信格式由上述自由决定。 您可以选择简约的类似GDB的界面,也可以选择控制台或图形用户界面。 如果需要,可以更改调试器命令的名称。
要解决此问题,您可以使用TIOBE TOP 50中的任何编程语言和开放源代码编译器/解释器。
在评估工作时将被评估:
方案的整体表现;
源代码的质量和测试的可用性;
易于扩展的功能(例如,支持新的语言语句或调试器指令)。
带有构建说明的解决方案必须发布到Git存储库中(例如,在GitHub或BitBucket上)。 在响应中,您需要指定到存储库的链接。 指向私有GitHub存储库的链接也是合适的,只有您需要将我添加到其中。
我用C ++,Java和对象Pascal编写。
最初,我有想法将所有内容都写在同一MPS中,但是我认为检查JB员工并不是很方便,因此我在提交结束前两天提交了申请(所有考试均相同...),并且窗外已经是傍晚了-我决定用更知名的语言快速编写所有内容。
在我看来,至少由于最方便的字符串实现,Pascal最适合解决问题。
至少对我来说。 另外,它在TIOBE TOP 50中,所以我大胆地启动了IDE,即Lazarus,因为 他不是商业人士:)并着手解决问题。
尽管他们给了JB多达7天的时间,但我还是花了一个小时才能完成该项目,结果该项目大约需要500行代码。
从哪里开始?
首先,您需要想象最后如何进行代码调试。
我们需要实现分步执行代码-这意味着每条指令应以结构/类的形式呈现,并且一般而言,这些指令应看起来像这些类的列表,或者在我的实现中,彼此参照形成一个序列(我会记下以后为什么要做)。
要获得此序列,我们的调试器需要使用建议的语言处理代码,这意味着我们还需要实现一个小的解析器以及代码的语法和语义分析。
让我们从解析器的实现开始。 因为 由于Guu语言由一组由空格分隔的标记组成,因此首先编写一个小型且简单的标记器是合乎逻辑的:
function GetToken(s: string; tokenNum: word): string; var p: word; begin s := Trim(s); s := StringReplace(s, ' ', ' ', [rfReplaceAll]); while tokenNum > 1 do begin p := Pos(' ', s); if p > 0 then Delete(s, 1, p) else begin s := ''; break; end; dec(tokenNum); end; p := Pos(' ', s); if p > 0 then Delete(s, p, Length(s)); Result := s; end;
接下来,从令牌声明枚举:
type TGuuToken = (opSub, opSet, opCall, opPrint, opUnknown); const GuuToken: array[opSub..opPrint] of string = ( 'sub', 'set', 'call', 'print' );
还有指令类本身,我们将在其中解析代码行:
type TGuuOp = class public OpType : TGuuToken; OpArgs : TStringList; OpLine : Cardinal; OpUnChangedLine: string; NextOp : TGuuOp; OpReg : Pointer; function Step(StepInto: boolean; CallBacks: TList; Trace: TStringList): TGuuOp; constructor Create(LineNum: Cardinal; Line:string); destructor Destroy; override; end;
在OpType中,指令将存储在OpArgs中-其余结构。
OpLine,OpUnChangedLine-调试器的信息。
NextOp是指向下一条语句的指针。 如果它等于nil(在Pascal中为null),则没有进一步的说明,您需要完成代码或通过回调堆栈返回。
OpReg是一个小型指针寄存器,稍后将用于代码执行的小型优化。
编写完类声明后,我决定最紧凑,最漂亮的解决方案是在其构造函数中添加解析器并进行一点解析,接下来我要做的是:
constructor TGuuOp.Create(LineNum: Cardinal; Line:string); var s: string; w: word; begin inherited Create; OpArgs := TStringList.Create; OpLine := LineNum; OpUnChangedLine := Line; NextOp := nil; OpReg := nil; s := GetToken(Line, 1); OpType := TGuuToken(AnsiIndexStr(s, GuuToken)); case OpType of opSub : begin
在这里,我们本质上检查了构造的开头(即第一个单词),然后查看了剩余的标记及其编号。 如果代码明显有问题,我们将显示错误。
在主要代码段中,我们仅从文件中读取TStringList中的代码,逐行调用TGuuOp构造函数,然后将指向类实例的指针存储在GuuOps:TList中。
公告:
var LabelNames: TStringList; GuuOps, GuuVars: TList; SubMain: TGuuOp = nil;
结合代码解析,最好再执行几个操作:
procedure ParseNext(LineNum: Cardinal; Line: string); var Op: TGuuOp; GV: TGuuVar; c: cardinal; begin if Trim(Line) <> '' then begin Op := TGuuOp.Create(LineNum, Line); GuuOps.Add(Op); case Op.OpType of opSet: begin
在此阶段,您可以在重新定义时检查入口点,并想到OpReg-我用它来存储指向Guu变量的指针。
说到变量,我将这小段代码放入了一个单独的单元中:
unit uVars; {$mode objfpc}{$H+} interface uses Classes, SysUtils; type TGuuVar = class public gvName: string; gvVal: variant; constructor Create(VarName: string); end; implementation constructor TGuuVar.Create(VarName: string); begin inherited Create; gvName := VarName; gvVal := 0; end; end.
现在,我们已经解析了语法正确的代码。 它仍然需要分析,您可以开始执行最重要的事情-调试。
接下来,您需要进行一些语义分析,并同时为代码执行和调试做好一切准备:
procedure CheckSemantic; var c, x: cardinal; op: TGuuOp; begin if GuuOps.Count > 0 then begin if TGuuOp(GuuOps[0]).OpType <> opSub then begin writeln('[Error]: Operation outside sub at line ', TGuuOp(GuuOps[0]).OpLine, '.'); halt; end; c := 0; while c < GuuOps.Count do begin case TGuuOp(GuuOps[c]).OpType of opSub:; opCall: begin TGuuOp(GuuOps[c - 1]).NextOp := TGuuOp(GuuOps[c]); x := 0; op := nil; while x < GuuOps.Count do begin if TGuuOp(GuuOps[x]).OpType = opSub then if TGuuOp(GuuOps[x]).OpArgs[0] = TGuuOp(GuuOps[c]).OpArgs[0] then begin op := TGuuOp(GuuOps[x]); break; end; inc(x); end; if op <> nil then TGuuOp(GuuOps[c]).OpReg := op else begin writeln('[Error]: Calling to not exist sub "', TGuuOp(GuuOps[c]).OpArgs[0], '" at line ', TGuuOp(GuuOps[c]).OpLine, '.'); halt; end; end; opPrint: begin TGuuOp(GuuOps[c - 1]).NextOp := TGuuOp(GuuOps[c]); x := 0; while x < GuuVars.Count do begin if TGuuVar(GuuVars[x]).gvName = TGuuOp(GuuOps[c]).OpArgs[0] then begin TGuuOp(GuuOps[c]).OpReg := TGuuVar(GuuVars[x]); break; end; inc(x); end; if TGuuOp(GuuOps[c]).OpReg = nil then begin writeln('[Error]: Variable "', TGuuOp(GuuOps[c]).OpArgs[0], '" for print doesn''t exist at line ', TGuuOp(GuuOps[c]).OpLine, '.'); end; end; else TGuuOp(GuuOps[c - 1]).NextOp := TGuuOp(GuuOps[c]); end; inc(c); end; end; end;
在每个令牌的TGuuOp.NextOp中,编写指向下一个令牌的指针。
对于调用操作码,我们要做到这一点既简单又棘手-在NextOp中,我们编写了一个指向被调用入口点的指针。
我们还通过print语句检查输出变量...
也许他们没有在结论之前宣布?
现在您需要实现代码执行。 返回TGuuOp类并实现Step方法:
function TGuuOp.Step(StepInto: boolean; CallBacks: TList; Trace: TStringList): TGuuOp; var Op: TGuuOp; CBSize: Cardinal; begin case OpType of opSub: begin Trace.Add('-> Sub "' + OpArgs[0] + '"'); Result := NextOp; end; opCall: begin if StepInto then begin if NextOp <> nil then CallBacks.Add(NextOp); Result := TGuuOp(OpReg); end else begin Op := TGuuOp(OpReg); CBSize := CallBacks.Count; while ((Op <> nil) or (CallBacks.Count > CBSize)) and (Trace.Count < STACK_SIZE) do begin if Op = nil then begin Op := TGuuOp(CallBacks[CallBacks.Count - 1]); CallBacks.Delete(CallBacks.Count - 1); Trace.Delete(Trace.Count - 1); end; Op := Op.Step(StepInto, CallBacks, Trace); end; Result := NextOp; end; end; opPrint: begin writeln(TGuuVar(OpReg).gvName, ' = ', TGuuVar(OpReg).gvVal); Result := NextOp; end; opSet: begin TGuuVar(OpReg).gvVal := OpArgs[1]; Result := NextOp; end; end; end;
为了避免在发生循环时发生访问冲突-最好限制堆栈,我这样做了。
上面声明的常量STACK_SIZE = 2048就是这个原因。
现在终于可以编写调试器的主要代码了:
var code: TStringList; c: Cardinal; cmd: string; CallBacks: TList; Trace: TStringList; DebugMode: boolean = true; begin if ParamCount > 0 then begin
根据工作条件,可以根据需要实现该接口。
可以实现一个完整的UI,将SynEdit固定到该项目中,但是在我看来,这是一项空洞的工作,无法反映其技能,此外,它不会付给您:)
因此,我将自己局限于小型控制台UI。
上面的代码并不复杂,因此您可以不加注释。 在其中,我们采用现成的TGuuOp,并将其称为Step。
已解决问题的屏幕截图:


错误信息输出:


链接到我的解决方案的存储库:
单击总结
没有特别的结果。 我必须将整个暑假都花在忙碌的假期和寻找一所大学上(当然,如果我顺利通过考试),而不是在JetBrains团队中工作和训练两个月。
也许明年,一条新文章将出现在哈布雷(Habré)上,已经描述了在JB或我感兴趣的另一家公司的实习过程:)