emacs에서 C #에 대한 완성 (지능형) 기능을 개발 중입니다.
아이디어는 사용자가 조각을 입력 한 다음 특정 키 입력 조합을 통해 완료를 요청하면 완료 기능이 .NET 리플렉션을 사용하여 가능한 완료를 결정한다는 것입니다.
이렇게하려면 완료되는 사물의 유형을 알아야합니다. 문자열 인 경우 알려진 가능한 메서드 및 속성 집합이 있습니다. Int32 인 경우 별도의 집합이 있습니다.
emacs에서 사용할 수있는 코드 렉서 / 파서 패키지 인 semantic을 사용하여 변수 선언과 해당 유형을 찾을 수 있습니다. 따라서 리플렉션을 사용하여 유형에 대한 메서드와 속성을 가져온 다음 사용자에게 옵션 목록을 제공하는 것은 간단합니다. (좋아, 꽤 간단 할 내 이맥스하지만, 사용 이맥스 내부의 파워 쉘 프로세스를 실행하는 능력을 , 훨씬 쉬워집니다. 나는 내에서 실행 elisp 후, 반사를 할 어셈블리 정의 .NET 쓰기 PowerShell을에로드하고, emacs는 comint를 통해 powershell에 명령을 보내고 응답을 읽을 수 있습니다. 따라서 emacs는 반영 결과를 빠르게 얻을 수 있습니다.)
코드 var
가 완료되는 것을 선언 할 때 문제가 발생합니다 . 즉, 유형이 명시 적으로 지정되지 않았으며 완성이 작동하지 않습니다.
변수를 var
키워드 로 선언 할 때 사용되는 실제 유형을 어떻게 안정적으로 결정할 수 있습니까? 명확히하기 위해 런타임에 결정할 필요가 없습니다. “디자인 타임”에서 결정하고 싶습니다.
지금까지 다음 아이디어가 있습니다.
- 컴파일 및 호출 :
- 선언문 추출, 예 :`var foo = “a string value”;`
- `foo.GetType ();`문을 연결합니다.
- 결과 C # 조각을 새 어셈블리로 동적으로 컴파일합니다.
- 어셈블리를 새 AppDomain에로드하고 조각을 실행하고 반환 유형을 가져옵니다.
- 어셈블리를 언로드 및 폐기
나는이 모든 것을하는 방법을 알고있다. 그러나 편집기의 각 완료 요청에 대해 매우 무겁게 들립니다.
매번 새로운 AppDomain이 필요하지 않다고 생각합니다. 여러 임시 어셈블리에 단일 AppDomain을 재사용하고 여러 완료 요청에서 설정 및 해체 비용을 분할 할 수 있습니다. 그것은 기본 아이디어의 수정입니다.
- IL 컴파일 및 검사
선언을 모듈로 컴파일 한 다음 IL을 검사하여 컴파일러에서 유추 한 실제 유형을 확인하면됩니다. 이것이 어떻게 가능할까요? IL을 검사하기 위해 무엇을 사용합니까?
더 나은 아이디어가 있습니까? 코멘트? 제안?
편집 -이것에 대해 더 생각하면 호출에 부작용이있을 수 있으므로 컴파일 및 호출이 허용되지 않습니다. 따라서 첫 번째 옵션은 배제되어야합니다.
또한 .NET 4.0의 존재를 짐작할 수 없다고 생각합니다.
업데이트 -위에서 언급하지 않았지만 Eric Lippert가 부드럽게 지적한 정답은 완전한 충실도 유형 추론 시스템을 구현하는 것입니다. 디자인 타임에 var의 유형을 안정적으로 결정하는 유일한 방법입니다. 그러나 그것은 또한 쉽지 않습니다. 그런 것을 만들고 싶다는 착각을 겪지 않았기 때문에 옵션 2의 지름길을 사용했습니다. 관련 선언 코드를 추출하고 컴파일 한 다음 결과 IL을 검사합니다.
이것은 완료 시나리오의 공정한 하위 집합에 대해 실제로 작동합니다.
예를 들어 다음 코드 조각에서? 사용자가 완료를 요청하는 위치입니다. 이것은 작동합니다 :
var x = "hello there";
x.?
완성은 x가 문자열임을 인식하고 적절한 옵션을 제공합니다. 다음 소스 코드를 생성하고 컴파일하여이를 수행합니다.
namespace N1 {
static class dmriiann5he { // randomly-generated class name
static void M1 () {
var x = "hello there";
}
}
}
… 간단한 반사로 IL을 검사합니다.
이것은 또한 작동합니다 :
var x = new XmlDocument();
x.?
엔진은 생성 된 소스 코드에 적절한 using 절을 추가하여 제대로 컴파일되고 IL 검사는 동일합니다.
이것도 작동합니다.
var x = "hello";
var y = x.ToCharArray();
var z = y.?
IL 검사가 첫 번째 대신 세 번째 지역 변수의 유형을 찾아야 함을 의미합니다.
이:
var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var x = z.?
… 이전 예제보다 한 단계 더 깊습니다.
그러나 작동 하지 않는 것은 초기화가 인스턴스 멤버 또는 로컬 메서드 인수의 어느 지점에서든 종속되는 로컬 변수에 대한 완료입니다. 처럼:
var foo = this.InstanceMethod();
foo.?
LINQ 구문도 아닙니다.
완성을 위해 확실히 “제한된 디자인”(정중 한 단어)을 통해 문제를 해결하는 것을 고려하기 전에 이러한 것들이 얼마나 가치가 있는지 생각해야합니다.
메서드 인수 또는 인스턴스 메서드에 대한 종속성 문제를 해결하는 방법은 생성되고 컴파일 된 다음 IL이 분석되는 코드 조각에서 이러한 항목에 대한 참조를 동일한 유형의 “합성”로컬 변수로 대체하는 것입니다.
또 다른 업데이트 -인스턴스 멤버에 의존하는 변수에 대한 완성이 이제 작동합니다.
내가 한 일은 (시맨틱을 통해) 유형을 조사한 다음 모든 기존 멤버에 대한 합성 대기 멤버를 생성하는 것입니다. 다음과 같은 C # 버퍼의 경우 :
public class CsharpCompletion
{
private static int PrivateStaticField1 = 17;
string InstanceMethod1(int index)
{
...lots of code here...
return result;
}
public void Run(int count)
{
var foo = "this is a string";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var mmm = count + z + CsharpCompletion.PrivateStaticField1;
var nnn = this.InstanceMethod1(mmm);
var fff = nnn.?
...more code here...
… 컴파일 된 생성 된 코드는 출력 IL에서 로컬 var nnn의 유형을 배울 수 있습니다.
namespace Nsbwhi0rdami {
class CsharpCompletion {
private static int PrivateStaticField1 = default(int);
string InstanceMethod1(int index) { return default(string); }
void M0zpstti30f4 (int count) {
var foo = "this is a string";
var fred = new System.Collections.Generic.List<String> { foo, foo.Length.ToString() };
var z = fred.Count;
var mmm = count + z + CsharpCompletion.PrivateStaticField1;
var nnn = this.InstanceMethod1(mmm);
}
}
}
모든 인스턴스 및 정적 유형 멤버는 스켈레톤 코드에서 사용할 수 있습니다. 성공적으로 컴파일됩니다. 이 시점에서 지역 변수의 유형을 결정하는 것은 Reflection을 통해 간단합니다.
이를 가능하게하는 것은 다음과 같습니다.
- emacs에서 powershell을 실행하는 기능
- C # 컴파일러는 정말 빠릅니다. 내 컴퓨터에서 메모리 내 어셈블리를 컴파일하는 데 약 0.5 초가 걸립니다. 키 입력 간 분석에는 빠르지 않지만 주문형 완성 목록 생성을 지원할만큼 빠릅니다.
아직 LINQ를 살펴 보지 않았습니다.
의미 론적 렉서 / 파서 emacs가 C #에 대해 가지고 있지만 LINQ를 “실행”하지 않기 때문에 훨씬 더 큰 문제가 될 것입니다.
답변
“실제”C # IDE에서이를 효율적으로 수행하는 방법을 설명 할 수 있습니다.
가장 먼저 할 일은 소스 코드의 “최상위”항목 만 분석하는 패스를 실행하는 것입니다. 모든 메서드 본문을 건너 뜁니다. 이를 통해 프로그램의 소스 코드에있는 네임 스페이스, 유형 및 메서드 (및 생성자 등)에 대한 정보 데이터베이스를 빠르게 구축 할 수 있습니다. 모든 메서드 본문에서 모든 코드 줄을 분석하는 것은 키 입력 사이에 작업을 수행하려는 경우 너무 오래 걸립니다.
IDE가 메서드 본문 내에서 특정 표현식의 유형을 파악해야하는 경우- “foo”를 입력했다고 가정 해 보겠습니다. 그리고 우리는 foo의 구성원이 무엇인지 알아 내야합니다. 우리는 같은 일을합니다. 합리적으로 할 수있는 한 많은 작업을 건너 뜁니다.
해당 메서드 내 에서 지역 변수 선언 만 분석하는 패스로 시작 합니다. 이 패스를 실행할 때 “범위”와 “이름”쌍에서 “유형 결정자”로 매핑을 만듭니다. “유형 결정자”는 “필요한 경우이 로컬 유형을 해결할 수 있습니다”라는 개념을 나타내는 개체입니다. 지역 유형을 작업하는 것은 비용이 많이들 수 있으므로 필요한 경우 작업을 연기하고 싶습니다.
이제 모든 로컬 유형을 알려줄 수있는 느리게 구축 된 데이터베이스가 있습니다. 그래서 그 “foo”로 돌아갑니다. -우리 는 관련 표현식이 어떤 문 에 있는지 알아 낸 다음 해당 문에 대해 의미 분석기를 실행합니다. 예를 들어 다음과 같은 메서드 본문이 있다고 가정합니다.
String x = "hello";
var y = x.ToCharArray();
var z = from foo in y where foo.
이제 우리는 foo가 char 유형이라는 것을 알아 내야합니다. 모든 메타 데이터, 확장 메서드, 소스 코드 유형 등을 포함하는 데이터베이스를 구축합니다. x, y 및 z에 대한 유형 결정자가있는 데이터베이스를 구축합니다. 흥미로운 표현이 담긴 진술을 분석합니다. 우리는 구문을 다음과 같이 변환하여 시작합니다.
var z = y.Where(foo=>foo.
foo 유형을 알아 내기 위해서는 먼저 y 유형을 알아야합니다. 그래서이 시점에서 우리는 유형 결정자에게 “y의 유형은 무엇입니까?”라고 묻습니다. 그런 다음 x.ToCharArray ()를 구문 분석하고 “x의 유형이 무엇인지”묻는 표현식 평가기를 시작합니다. “현재 컨텍스트에서”문자열 “을 조회해야합니다”라는 유형 결정자가 있습니다. 현재 유형에는 String 유형이 없으므로 네임 스페이스를 살펴 봅니다. 그것도 존재하지 않기 때문에 using 지시문을 살펴보고 “using System”이 있고 System이 String 유형을 가지고 있음을 발견합니다. 좋습니다. 이것이 x의 유형입니다.
그런 다음 System.String의 메타 데이터에 ToCharArray 유형을 쿼리하면 System.Char []라고 표시됩니다. 감독자. 그래서 우리는 y에 대한 유형을 가지고 있습니다.
이제 “System.Char []에 Where 메서드가 있습니까?”라고 묻습니다. 아니요. 따라서 using 지시문을 살펴 봅니다. 사용할 수있는 확장 메서드에 대한 모든 메타 데이터를 포함하는 데이터베이스를 이미 미리 계산했습니다.
이제 “OK, Where in scope라는 이름의 확장 메서드가 18 개 있습니다. 그 중 어떤 형식이 System.Char []와 호환되는 첫 번째 형식 매개 변수가 있습니까?” 그래서 우리는 전환성 테스트를 시작합니다. 그러나 Where 확장 메서드는 일반적 이므로 형식 유추를 수행해야합니다.
첫 번째 인수에서 확장 메서드에 대한 불완전한 추론을 처리 할 수있는 특수 유형 추론 엔진을 작성했습니다. 유형 추론기를 실행하고를받는 Where 메서드 IEnumerable<T>
가 있고 System.Char []에서으로 추론 할 수 IEnumerable<System.Char>
있으므로 T는 System.Char입니다.
이 메서드의 Where<T>(this IEnumerable<T> items, Func<T, bool> predicate)
서명은이며 T가 System.Char임을 알고 있습니다. 또한 확장 메서드에 대한 괄호 안의 첫 번째 인수가 람다라는 것을 알고 있습니다. 그래서 우리는 “정식 매개 변수 foo는 System.Char라고 가정한다”는 람다 표현식 유형 추론기를 시작합니다. 나머지 람다를 분석 할 때이 사실을 사용합니다.
이제 “foo.”인 람다의 본문을 분석하는 데 필요한 모든 정보를 얻었습니다. 우리는 foo의 유형을 찾고 람다 바인더에 따르면 System.Char라는 것을 발견하고 끝났습니다. System.Char에 대한 유형 정보를 표시합니다.
그리고 키 입력 사이 의 “최상위”분석을 제외한 모든 작업을 수행 합니다. 그것은 정말 까다로운 부분입니다. 실제로 모든 분석을 작성하는 것은 어렵지 않습니다. 그것을 만들고있다 충분히 빨리 당신이 진짜 까다로운 비트입니다 타이핑 속도로 그것을 할 수있다.
행운을 빕니다!
답변
Delphi IDE가 Delphi 컴파일러와 함께 작동하여 IntelliSense를 수행하는 방법을 대략적으로 설명 할 수 있습니다 (코드 통찰력은 Delphi에서 부르는 것입니다). C #에 100 % 적용 할 수는 없지만 고려해야 할 흥미로운 접근 방식입니다.
Delphi의 대부분의 의미 분석은 파서 자체에서 수행됩니다. 식은 구문 분석 될 때 형식이 지정됩니다. 단, 쉽지 않은 상황은 예외입니다.이 경우 미리보기 구문 분석을 사용하여 의도 한 내용을 파악한 다음 해당 결정이 구문 분석에 사용됩니다.
구문 분석은 연산자 우선 순위를 사용하여 구문 분석되는 표현식을 제외하고 대체로 LL (2) 재귀 하강입니다. Delphi의 뚜렷한 점 중 하나는 단일 패스 언어이므로 사용하기 전에 구성을 선언해야하므로 해당 정보를 가져 오는 데 최상위 패스가 필요하지 않습니다.
이러한 기능의 조합은 파서가 필요한 모든 지점에 대한 코드 통찰력에 필요한 대략적인 모든 정보를 가지고 있음을 의미합니다. 작동 방식은 다음과 같습니다. IDE는 컴파일러의 렉서에게 커서의 위치 (코드 통찰력이 필요한 지점)를 알리고 렉서는이를 특수 토큰 (키 비츠 토큰이라고 함)으로 변환합니다. 파서가이 토큰 (어디서나있을 수 있음)을 만날 때마다 이것이 가지고있는 모든 정보를 편집기로 되돌려 보내는 신호임을 압니다. C로 작성 되었기 때문에 longjmp를 사용하여이를 수행합니다. 그것이하는 일은 궁극적 인 호출자에게 kibitz 포인트가 발견 된 종류의 구문 구조 (즉, 문법적 문맥)와 그 포인트에 필요한 모든 상징적 테이블을 통지하는 것입니다. 예를 들어 컨텍스트가 메서드에 대한 인수 인 식에있는 경우 메서드 오버로드를 확인하고 인수 형식을 살펴보고 해당 인수 형식으로 확인할 수있는 기호로만 유효한 기호를 필터링 할 수 있습니다. 드롭 다운에 무관 한 잔해가 많이 있음). 중첩 된 범위 컨텍스트 (예 : “.”뒤)에있는 경우 파서는 범위에 대한 참조를 다시 전달하고 IDE는 해당 범위에서 찾은 모든 기호를 열거 할 수 있습니다.
다른 작업도 수행됩니다. 예를 들어, kibitz 토큰이 범위에 있지 않으면 메서드 본문을 건너 뜁니다. 이것은 낙관적으로 수행되고 토큰을 건너 뛰면 롤백됩니다. 확장 메서드 (Delphi의 클래스 도우미)에 해당하는 것은 일종의 버전 캐시를 가지고 있으므로 조회가 상당히 빠릅니다. 그러나 Delphi의 제네릭 유형 추론은 C #보다 훨씬 약합니다.
이제 구체적인 질문에 대해 :로 선언 된 변수 유형을 추론하는 것은 var
파스칼이 상수 유형을 추론하는 방식과 동일합니다. 초기화 표현식의 유형에서 비롯됩니다. 이러한 유형은 아래에서 위로 구성됩니다. 경우 x
유형 인 Integer
및 y
유형 인 Double
다음 x + y
형식이 될 것입니다 Double
그 언어의 규칙이기 때문에; 등. 오른쪽에 전체 표현식에 대한 유형이있을 때까지이 규칙을 따르고 왼쪽에있는 기호에 사용하는 유형입니다.
답변
추상 구문 트리를 구축하기 위해 자체 파서를 작성하지 않으려면 오픈 소스 인 SharpDevelop 또는 MonoDevelop 의 파서를 사용하는 방법을 살펴볼 수 있습니다.
답변
Intellisense 시스템은 일반적으로 컴파일러와 거의 동일한 방식으로 ‘var’변수에 할당되는 함수의 반환 유형을 확인할 수있는 추상 구문 트리를 사용하여 코드를 나타냅니다. VS Intellisense를 사용하는 경우 유효한 (해결 가능한) 할당 식 입력을 완료 할 때까지 var 유형을 제공하지 않을 수 있습니다. 식이 여전히 모호한 경우 (예 : 식에 대한 일반 인수를 완전히 추론 할 수 없음) var 유형은 확인되지 않습니다. 유형을 해결하려면 트리에 상당히 깊이 들어가야 할 수 있으므로 상당히 복잡한 프로세스가 될 수 있습니다. 예를 들면 :
var items = myList.OfType<Foo>().Select(foo => foo.Bar);
반환 유형은 IEnumerable<Bar>
이지만이 문제를 해결하려면 다음을 알아야합니다.
- myList는
IEnumerable
. OfType<T>
IEnumerable에 적용되는 확장 방법 이 있습니다.- 결과 값은
IEnumerable<Foo>
이고 여기Select
에 적용되는 확장 방법 이 있습니다. - 람다 식
foo => foo.Bar
에는 Foo 유형의 매개 변수 foo가 있습니다. 이것은 a를 취하는 Select의 사용법에 의해 추론되며Func<TIn,TOut>
TIn이 알려져 있기 때문에 (Foo) foo의 유형을 추론 할 수 있습니다. - Foo 유형에는 Bar 유형의 속성 Bar가 있습니다.
IEnumerable<TOut>
람다 식의 결과에서 Select 반환 및 TOut을 유추 할 수 있으므로 결과 항목 유형은IEnumerable<Bar>
.
답변
Emacs를 목표로하고 있기 때문에 CEDET 제품군으로 시작하는 것이 가장 좋습니다. Eric Lippert의 모든 세부 사항은 C ++ 용 CEDET / Semantic 도구의 코드 분석기에서 이미 다루었습니다. C # 파서 (약간의 TLC가 필요할 수 있음)도 있으므로 누락 된 부분은 C #에 필요한 부분을 조정하는 것과 관련이 있습니다.
기본 동작은 언어별로 정의 된 오버로드 가능한 함수에 의존하는 핵심 알고리즘에서 정의됩니다. 완성 엔진의 성공 여부는 얼마나 많은 튜닝이 수행되었는지에 달려 있습니다. C ++를 가이드로 사용하면 C ++와 유사한 지원을받는 것이 그리 나쁘지 않습니다.
Daniel의 대답은 MonoDevelop을 사용하여 구문 분석 및 분석을 수행하는 것을 제안합니다. 이는 기존 C # 파서 대신 대체 메커니즘이거나 기존 파서를 확장하는 데 사용할 수 있습니다.
답변
잘하는 것은 어려운 문제입니다. 기본적으로 대부분의 렉싱 / 파싱 / 타입 검사를 통해 언어 사양 / 컴파일러를 모델링하고 쿼리 할 수있는 소스 코드의 내부 모델을 빌드해야합니다. Eric은 C #에 대해 자세히 설명합니다. 언제든지 F # 컴파일러 소스 코드 (F # CTP의 일부)를 다운로드하고 F # service.fsi
언어 서비스가 인텔리 센스, 유추 된 형식에 대한 도구 설명 등을 제공하는 데 사용하는 F # 컴파일러에서 노출 된 인터페이스를 살펴볼 수 있습니다. 호출 할 API로 컴파일러를 이미 사용할 수있는 경우 가능한 ‘인터페이스’에 대한 감각.
다른 방법은 설명하는 그대로 컴파일러를 재사용 한 다음 리플렉션을 사용하거나 생성 된 코드를 보는 것입니다. 이것은 컴파일러에서 컴파일 출력을 얻기 위해 ‘전체 프로그램’이 필요하다는 관점에서 문제가되는 반면, 편집기에서 소스 코드를 편집 할 때 아직 구문 분석하지 않은 ‘부분 프로그램’만있는 경우가 많습니다. 모든 방법이 아직 구현되지 않았습니다.
간단히 말해서, ‘저예산’버전은 잘하기가 매우 어렵고 ‘실제’버전은 잘하기가 매우 어렵다고 생각합니다. (여기서 ‘하드’는 ‘노력’과 ‘기술적 어려움’을 모두 측정합니다.)