Welcome
Welcome to nn-lang blog
nn-lang tag description
모든 태그 보기Welcome to nn-lang blog
SieR-VR blog에서 가져온 글입니다.
이번 글은 언어 서버에 대해 설명하려 한다.
다만 예전 구현은 별로 설명할만한 구석이 없어서 현재(2024-10-03) 기준으로 글을 작성하였다.
또한, 구현 중 참고했던 Typescript Language Server (이하 TSServer)코드도 같이 설명했으니 알아두길 바란다.
큰 틀에서의 구조는 TSServer와 크게 다르지 않은데,
export function createLspConnection(options: LspConnectionOptions) {
const connection = createConnection(ProposedFeatures.all);
const client = new LspClient(connection);
const logger = new LspClientLogger(client, options.showMessageLevel);
const documents = new TextDocuments(TextDocument);
const context: Partial<LspContext> = {
logger,
client,
documents,
showMessageLevel: options.showMessageLevel
}
connection.onDidOpenTextDocument((params) => openTextDocument(params, context as LspContext))
connection.onDidCloseTextDocument((params) => onDidCloseTextDocument(params, context as LspContext))
documents.onDidChangeContent((params) => onDidChangeTextDocument(params, context as LspContext))
connection.onInitialize((params) => initialize(params, context))
connection.onCompletion((params, token) => completion(params, context as LspContext, token))
connection.onHover((params, token) => hover(params, context as LspContext, token))
connection.languages.semanticTokens.on((params) => semanticTokens(params, context as LspContext))
documents.listen(connection);
return connection;
}
export function createLspConnection(options: LspConnectionOptions): lsp.Connection {
const connection = lsp.createConnection(lsp.ProposedFeatures.all);
const lspClient = new LspClientImpl(connection);
const logger = new LspClientLogger(lspClient, options.showMessageLevel);
const server: LspServer = new LspServer({
logger,
lspClient,
});
connection.onInitialize(server.initialize.bind(server));
connection.onInitialized(server.initialized.bind(server));
connection.onDidChangeConfiguration(server.didChangeConfiguration.bind(server));
connection.onDidOpenTextDocument(server.didOpenTextDocument.bind(server));
connection.onDidSaveTextDocument(server.didSaveTextDocument.bind(server));
connection.onDidCloseTextDocument(server.didCloseTextDocument.bind(server));
connection.onDidChangeTextDocument(server.didChangeTextDocument.bind(server));
connection.onCodeAction(server.codeAction.bind(server));
connection.onCodeLens(server.codeLens.bind(server));
connection.onCodeLensResolve(server.codeLensResolve.bind(server));
connection.onCompletion(server.completion.bind(server));
connection.onCompletionResolve(server.completionResolve.bind(server));
connection.onDefinition(server.definition.bind(server));
connection.onImplementation(server.implementation.bind(server));
connection.onTypeDefinition(server.typeDefinition.bind(server));
connection.onDocumentFormatting(server.documentFormatting.bind(server));
connection.onDocumentRangeFormatting(server.documentRangeFormatting.bind(server));
connection.onDocumentHighlight(server.documentHighlight.bind(server));
connection.onDocumentSymbol(server.documentSymbol.bind(server));
connection.onExecuteCommand(server.executeCommand.bind(server));
connection.onHover(server.hover.bind(server));
connection.onReferences(server.references.bind(server));
connection.onRenameRequest(server.rename.bind(server));
connection.onPrepareRename(server.prepareRename.bind(server));
connection.onSelectionRanges(server.selectionRanges.bind(server));
connection.onSignatureHelp(server.signatureHelp.bind(server));
connection.onWorkspaceSymbol(server.workspaceSymbol.bind(server));
connection.onFoldingRanges(server.foldingRanges.bind(server));
connection.languages.onLinkedEditingRange(server.linkedEditingRange.bind(server));
connection.languages.callHierarchy.onPrepare(server.prepareCallHierarchy.bind(server));
connection.languages.callHierarchy.onIncomingCalls(server.callHierarchyIncomingCalls.bind(server));
connection.languages.callHierarchy.onOutgoingCalls(server.callHierarchyOutgoingCalls.bind(server));
connection.languages.inlayHint.on(server.inlayHints.bind(server));
connection.languages.semanticTokens.on(server.semanticTokensFull.bind(server));
connection.languages.semanticTokens.onRange(server.semanticTokensRange.bind(server));
connection.workspace.onWillRenameFiles(server.willRenameFiles.bind(server));
return connection;
}
위쪽 코드가 nn의 lsp 메소드별 라우팅, 아래쪽 코드가 TSServer가 구현하고 있는 lsp 메소드별 라우팅이다.
다만 라우팅 하위 코드를 TSServer에서는 class로 구현하고 있는 반면,
export async function hover(params: TextDocumentPositionParams, context: LspContext, token?: CancellationToken): Promise<Hover | null> {
nn 구현에서는 context 객체를 따로 만들어서 인자로 넣어주고 있다. (큰 이유는 없지만 class 문법의 장황함과 indent가 맘에 안들었다.)
그 외 설명할만한 포인트가 하나 있는데,
private triggerDiagnostics(delay: number = 200): void {
this.diagnosticDelayer.trigger(() => {
this.sendPendingDiagnostics();
}, delay);
}
diagnostic을 전달하기 전에 일부러 200ms의 딜레이를 준다.
이유는 사용자의 키 스트로크가 진행되는 중에는 diagnostic이 실시간으로 변하지 않게 하기 위함이다.
(이 기능 때문에 TSServer에서 diagnostic을 전달하는 코드가 엄청 복잡해졌는데, 간단히 설명하자면 이미 발생한 요청을 취소하기 위해 최소 2~3개의 클래스를 더 만들어야 했다.)
해당 기능도 구현하기로 마음을 먹어
export class Delayer<T> {
constructor(
public defaultDelay: number,
private timeout: NodeJS.Timeout | null = null,
private completionPromise: Promise<T> | null = null,
private onSuccess: ((value: T) => void) | null = null,
private task: (() => T) | null = null,
)
{
}
public trigger(task: () => T, delay: number = this.defaultDelay): Promise<T> {
this.task = task;
if (delay >= 0) {
this.cancelTimeout();
}
if (!this.completionPromise) {
this.completionPromise = new Promise<T>((resolve) => {
this.onSuccess = resolve;
}).then(() => {
this.completionPromise = null;
const result = this.task!();
this.task = null;
return result;
});
}
if (delay >= 0 || this.timeout === null) {
this.timeout = setTimeout(() => {
this.timeout = null;
this.onSuccess!(null!);
}, delay >= 0 ? delay : this.defaultDelay);
}
return this.completionPromise;
}
public cancelTimeout(): void {
if (this.timeout !== null) {
clearTimeout(this.timeout);
this.timeout = null;
}
}
}
delayer 코드를 구현하고 적용했다.
아무래도 아직 구현할 게 많기 때문에, 나중에 관련해서 글을 또 써야할 것 같다.
다음 주제는 (아마도) 타입 체커 세부 구현일 것 같다.
SieR-VR blog에서 가져온 글입니다.
파서와 간단한 코드젠(트랜스폼)을 만들고 그 다음에 만들어야겠다 생각한 건 타입 체커였다.
사실상의 메인 피쳐이고, 가장 구현하는 데 시간이 오래 걸렸다.
export interface FileScope {
path: string;
declarations: DeclarationScope[];
}
export interface DeclarationScope {
file: FileScope;
declaration: string;
sizes: Size[];
values: Value[];
}
export interface Size {
scope: DeclarationScope;
ident: string;
nodes: Set<Node>;
}
export interface Value {
scope: DeclarationScope;
ident: string;
nodes: Set<Node>;
}
Name Resolver에 사용할 타입들의 정의이다.
구현은 이 타입 정의에 따라 내부 값들을 채워넣는 게 전부였다.
for (const decl of sourceCode) {
const declScope: DeclarationScope = {
file: fileScope,
declaration: decl.name.value,
sizes: [],
values: []
}
decl.sizeDeclList.decls
.forEach(ident => {
const sizeScope = toSize(declScope, ident);
declScope.sizes.push(sizeScope);
});
decl.argumentList.args
.forEach(arg => {
const valueScope = toValue(declScope, arg.ident);
declScope.values.push(valueScope);
});
const callExpressions = travel(decl.exprs, isCallExpression);
const identExprs = travel(decl.exprs, isIdentifierExpression);
identExprs
.forEach(identExpr => {
const value = findValue(declScope, identExpr.ident.value);
if (value) {
value.nodes.add(identExpr.ident);
} else {
const newValue = toValue(declScope, identExpr.ident);
declScope.values.push(newValue);
}
});
callExpressions
.flatMap(sizeDeclList => sizeDeclList.sizes)
.filter(ident => !!ident)
.filter(ident => typeof ident !== "number")
.forEach(ident => {
const size = findSize(declScope, ident.value);
if (size) {
size.nodes.add(ident);
} else {
const newSize = toSize(declScope, ident);
declScope.sizes.push(newSize);
}
});
fileScope.declarations.push(declScope);
}
처음 구현했던 name revoler 코드이다.
위 함수를 구현하고 느낀 건데, 생각했던 것 만큼 구현 과정이 어렵진 않았다.
이라는 큰 틀이 잡히기 시작했다.
그 외 큰 변경점이라면, yarn monorepo를 활용해 모듈을 크게 세 개로 분리했다.
대략 이런 형태인데, 이 중 언어 서버 구현에 관한 내용을 다음 글에서 풀어볼까 한다.
SieR-VR blog에서 가져온 글입니다.
nn이라는 언어를 처음 고안한건 2022년 말~2023년 초 쯤이었다.
다만 그 당시에는 훨씬 범용 프로그래밍 언어적인 성격이 강했는데,
entry(x: Tensor, a: Tensor, b: Tensor) {
let x = MatMul(x, a);
let x = Bias(x, b);
x
}
Rust의 서브셋같은 느낌으로 처음 구현했었다. (Python)
다만 만들다보니 파이썬을 선택한 의미가 많이 퇴색됐고, (프레임워크 호환성을 위해 선택했었다.)
파이썬 자체 문제 (익숙하지 않은 타이핑 방식, 라이브러리의 부실한 타이핑 및 문서)로 인해 사실상 개발이 많이 어려워져 버려두고 있었다. (당시 코드는 여기에서 확인할 수 있다.)
그 사이에 함수형 언어들을 접하면서 문법을 바라보는 시각도 조금 달라졌었다.
올해에는 꼭 만들어보자 하고 다짐했었는데, 올해 초에는 새 직장에도 적응해야 했고, 개인적으로 다시 잡을만한 시간이 안 나서 이제야 개발에 손을 대고 있다.
기존에 파이썬으로 구현한 경험이 있어서 새삼 느끼지만, 역시 Typescript로 구현하는 편이 훨씬 편하고 코드 정리하기도 쉬웠다.
아무튼, 이 개발기는 Typescript로 작성한 코드 기준으로 쓰일 예정이다. 개변 이전의 역사도 가끔 언급될지도 모르겠지만..
맨 처음 proof-of-concept는 ohm.js를 이용하여 만들어졌다.
당시 ohmjs 문법 파일은 이런 형태였다.
SourceCode {
Declaration = identifier SizeDecls? Arguments "=" "|>"? Expression ("|>" Expression)*
Expression = TupleExpression
StringLiteralExpression = string
IdentifierExpression = identifier -- ident
| StringLiteralExpression
CallExpression = identifier SizeType? "(" ListOf<Expression, ","> ")" -- call
| IdentifierExpression
TupleExpression = Expression ( "," Expression ) + -- tuple
| CallExpression
Arguments = "(" ListOf<ArgumentDeclaration, ","> ")"
ArgumentDeclaration = identifier ":" Type
Type = identifier SizeType?
SizeType = "[" ListOf<Size, ","> "]"
Size = PowerSize
SizeDecls = "[" ListOf<identifier, ","> "]"
SimpleSize = number | identifier
ParenSize = "(" Size ")" -- paren
| SimpleSize
AddSize = Size "+" Size -- add
| ParenSize
MultipleSize = Size "*" Size -- mul
| AddSize
PowerSize = Size "^" Size -- pow
| MultipleSize
string = singleQuoteString | doubleQuoteString
singleQuoteString = "'" identifier "'"
doubleQuoteString = "\"" identifier "\""
identifier = identifierName
identifierName = identifierStart identifierPart*
identifierStart = "_" | "$" | letter
identifierPart = identifierStart | digit
number = "1".."9" digit*
}
tree-sitter를 사용하는 지금도 큰 틀에서 벗어나지는 않는다.
Linear(x: Tensor) =
x, Trainable('weight')
|> MatMul(), Trainable('bias')
|> Bias()
당시 작성했었던 Linear 코드이다.
또한 간단하게라도 Python 코드젠을 만들었는데,
위 코드에 사이즈 정보를 더해 컴파일하면
class Linear:
def __init__(self, input, channel):
self.weight = Tensor.zeros(input, channel)
self.bias = Tensor.zeros(channel)
def __call__(self, x: Tensor):
y = MatMul(x, self.weight)
y = Bias(y, self.bias)
return y
와 같이 간단하게 코드젠이 이루어지는 정도까지는 만들었다.
코드젠 코드는 여기에서 읽어볼 수 있다.