<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          探索類型系統(tǒng)的底層 - 自己實(shí)現(xiàn)一個(gè) TypeScript(硬核干貨)

          共 28350字,需瀏覽 57分鐘

           ·

          2021-06-25 07:30

          原文:https://indepth.dev/under-the-hood-of-type-systems/

          DawnL 譯

          這篇文章包含兩個(gè)部分:

          A 部分:類型系統(tǒng)編譯器概述(包括 TypeScript)

          • Syntax vs Semantics 語(yǔ)法 vs 語(yǔ)義
          • What is AST? 什么是 AST?
          • Types of compilers 編譯器的類型
          • What does a language compiler do? 語(yǔ)言編譯器是做什么的?
          • How does a language compiler work? 語(yǔ)言編譯器是如何工作的?
          • Type system compiler jobs 類型系統(tǒng)編譯器職責(zé)
          • Advanced type checker features 高級(jí)類型檢查器的功能

          B 部分:構(gòu)建我們自己的類型系統(tǒng)編譯器

          • The parser 解析器
          • The checker 檢查器
          • Running our compiler 運(yùn)行我們的編譯器
          • What have we missed? 我們遺漏了什么?

          A 部分:類型系統(tǒng)編譯器概述

          語(yǔ)法 vs 語(yǔ)義

          語(yǔ)法和語(yǔ)義之間的區(qū)別對(duì)于早期的運(yùn)行很重要。

          語(yǔ)法 - Syntax

          語(yǔ)法通常是指 JavaScript 本機(jī)代碼。本質(zhì)上是詢問(wèn)給定的 JavaScript 代碼在運(yùn)行時(shí)是否正確。

          例如,下面的語(yǔ)法是正確的:

          var foo: number = "not a number";

          語(yǔ)義 - Semantics

          這是特定于類型系統(tǒng)的代碼。本質(zhì)上是詢問(wèn)附加到代碼中的給定類型是否正確。

          例如,上面的代碼在語(yǔ)法上是正確的,但在語(yǔ)義上是錯(cuò)誤的(將變量定義為一個(gè)數(shù)字類型,但是值是一個(gè)字符串)。

          接下來(lái)是 JavaScript 生態(tài)系統(tǒng)中的 AST 和編譯器。

          什么是 AST?

          在進(jìn)一步討論之前,我們需要快速了解一下 JavaScript 編譯器中的一個(gè)重要機(jī)制 AST。

          關(guān)于 AST 詳細(xì)介紹請(qǐng)看這篇文章。

          AST 的意思是抽象語(yǔ)法樹(shù) ,它是一個(gè)表示程序代碼的節(jié)點(diǎn)樹(shù)。Node 是最小單元,基本上是一個(gè)具有 type 和 location 屬性的 POJO(即普通 JavaScript 對(duì)象)。所有節(jié)點(diǎn)都有這兩個(gè)屬性,但根據(jù)類型,它們也可以具有其他各種屬性。

          在 AST 格式中,代碼非常容易操作,因此可以執(zhí)行添加、刪除甚至替換等操作。

          例如下面這段代碼:

          function add(number{
            return number + 1;
          }

          將解析成以下 AST:

          編譯器類型

          在 JavaScript 生態(tài)系統(tǒng)中有兩種主要的編譯器類型:

          1. 原生編譯器(Native compiler)

          原生編譯器將代碼轉(zhuǎn)換為可由服務(wù)器或計(jì)算機(jī)運(yùn)行的代碼格式(即機(jī)器代碼)。類似于 Java 生態(tài)系統(tǒng)中的編譯器 - 將代碼轉(zhuǎn)換為字節(jié)碼,然后將字節(jié)碼轉(zhuǎn)換為本機(jī)代碼。

          2. 語(yǔ)言編譯器

          語(yǔ)言編譯器扮演著不同的角色。TypeScript 和 Flow 的編譯器在將代碼輸出到 JavaScript 時(shí)都算作語(yǔ)言編譯器。

          語(yǔ)言編譯器與原生編譯器的主要區(qū)別在于,前者的編譯目的是 tooling-sake(例如優(yōu)化代碼性能或添加附加功能),而不是為了生成機(jī)器代碼。

          語(yǔ)言編譯器是做什么的?

          在類型系統(tǒng)編譯器中,總結(jié)的兩個(gè)最基本的核心職責(zé)是:

          1. 執(zhí)行類型檢查

          引入類型(通常是通過(guò)顯式注解或隱式推理),以及檢查一種類型是否匹配另一種類型的方法,例如 string 和 number

          2. 運(yùn)行語(yǔ)言服務(wù)器

          對(duì)于一個(gè)在開(kāi)發(fā)環(huán)境中工作的類型系統(tǒng)(type system)來(lái)說(shuō),最好能在 IDE 中運(yùn)行任何類型檢查,并為用戶提供即時(shí)反饋。

          語(yǔ)言服務(wù)器將類型系統(tǒng)連接到 IDE,它們可以在后臺(tái)運(yùn)行編譯器,并在用戶保存文件時(shí)重新運(yùn)行。流行的語(yǔ)言,如 TypeScript 和 Flow 都包含一個(gè)語(yǔ)言服務(wù)器。

          3. 代碼轉(zhuǎn)換

          許多類型系統(tǒng)包含原生 JavaScript 不支持的代碼(例如不支持類型注解) ,因此它們必須將不受支持的 JavaScript 轉(zhuǎn)換為受支持的 JavaScript 代碼。

          關(guān)于代碼轉(zhuǎn)換更詳細(xì)的介紹,可以參考原作者的這兩篇文章 Web Bundler 和 Source Maps。

          語(yǔ)言編譯器是如何工作的?

          對(duì)于大多數(shù)編譯器來(lái)說(shuō),在某種形式上有三個(gè)共同的階段。

          1. 將源代碼解析為 AST

          • 詞法分析 -> 將代碼字符串轉(zhuǎn)換為令牌流(即數(shù)組)
          • 語(yǔ)法分析 -> 將令牌流轉(zhuǎn)換為 AST 表示形式

          解析器檢查給定代碼的語(yǔ)法。類型系統(tǒng)必須有自己的解析器,通常包含數(shù)千行代碼。

          Babel 解析器 中的 2200+ 行代碼,僅用于處理 statement 語(yǔ)句(請(qǐng)參閱此處)。

          Hegel 解析器將 typeAnnotation 屬性設(shè)置為具有類型注解的代碼(可以在這里看到)。

          TypeScript 的解析器擁有 8900+ 行代碼(這里是它開(kāi)始遍歷樹(shù)的地方)。它包含了一個(gè)完整的 JavaScript 超集,所有這些都需要解析器來(lái)理解。

          2. 在 AST 上轉(zhuǎn)換節(jié)點(diǎn)

          • 操作 AST 節(jié)點(diǎn)

          這里將執(zhí)行應(yīng)用于 AST 的任何轉(zhuǎn)換。

          3. 生成源代碼

          • 將 AST 轉(zhuǎn)換為 JavaScript 源代碼字符串

          類型系統(tǒng)必須將任何非 js 兼容的 AST 映射回原生 JavaScript。

          類型系統(tǒng)如何處理這種情況呢?

          類型系統(tǒng)編譯器(compiler)職責(zé)

          除了上述步驟之外,類型系統(tǒng)編譯器通常還會(huì)在解析之后包括一個(gè)或兩個(gè)額外步驟,其中包括特定于類型的工作。

          順便說(shuō)一下,TypeScript 的編譯器實(shí)際上有 5 個(gè)階段,它們是:

          1. 語(yǔ)言服務(wù)預(yù)處理器 - Language server pre-processor
          2. 解析器 - Parser
          3. 結(jié)合器 - Binder
          4. 檢查器 - Checker
          5. 發(fā)射器 - Emitter

          正如上面看到的,語(yǔ)言服務(wù)器包含一個(gè)預(yù)處理器,它觸發(fā)類型編譯器只在已更改的文件上運(yùn)行。這會(huì)監(jiān)聽(tīng)任意的 import 語(yǔ)句,來(lái)確定還有哪些內(nèi)容可能發(fā)生了更改,并且需要在下次重新運(yùn)行時(shí)攜帶這些內(nèi)容。

          此外,編譯器只能重新處理 AST 結(jié)構(gòu)中已更改的分支。關(guān)于更多 lazy compilation,請(qǐng)參閱下文。

          類型系統(tǒng)編譯器有兩個(gè)常見(jiàn)的職責(zé):

          1. 推導(dǎo) - Inferring

          對(duì)于沒(méi)有注解的代碼需要進(jìn)行推斷。關(guān)于這點(diǎn),這里推薦一篇關(guān)于何時(shí)使用類型注解和何時(shí)讓引擎使用推斷的文章。

          使用預(yù)定義的算法,引擎將計(jì)算給定變量或者函數(shù)的類型。

          TypeScript 在其 Binding 階段(兩次語(yǔ)義傳遞中的第一次)中使用最佳公共類型算法。它考慮每個(gè)候選類型并選擇與所有其他候選類型兼容的類型。上下文類型在這里起作用,也會(huì)做為最佳通用類型的候選類型。在這里的 TypeScript 規(guī)范中有更多的幫助。

          let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];

          TypeScript 實(shí)際上引入了 Symbols(interface)的概念,這些命名聲明將 AST 中的聲明節(jié)點(diǎn)與其他聲明進(jìn)行連接,從而形成相同的實(shí)體。它們是 TypeScript 語(yǔ)義系統(tǒng)的基本構(gòu)成。

          2. 檢查 - Checking

          現(xiàn)在類型推斷已經(jīng)完成,類型已經(jīng)分配,引擎可以運(yùn)行它的類型檢查。他們檢查給定代碼的 semantics。這些類型的檢查有很多種,從類型錯(cuò)誤匹配到類型不存在。

          對(duì)于 TypeScript 來(lái)說(shuō),這是 Checker (第二個(gè)語(yǔ)義傳遞) ,它有 20000+ 行代碼。

          我覺(jué)得這給出了一個(gè)非常強(qiáng)大的 idea,即在如此多的不同場(chǎng)景中檢查如此多的不同類型是多么的復(fù)雜和困難。

          類型檢查器不依賴于調(diào)用代碼,即如果一個(gè)文件中的任何代碼被執(zhí)行(例如,在運(yùn)行時(shí))。類型檢查器將處理給定文件中的每一行,并運(yùn)行適當(dāng)?shù)臋z查。

          高級(jí)類型檢查器功能

          由于這些概念的復(fù)雜性,我們今天不深入探討以下幾個(gè)概念:

          懶編譯 - Lazy compilation

          現(xiàn)代編譯的一個(gè)共同特征是延遲加載。他們不會(huì)重新計(jì)算或重新編譯文件或 AST 分支,除非絕對(duì)需要。

          TypeScript 預(yù)處理程序可以使用緩存在內(nèi)存中的前一次運(yùn)行的 AST 代碼。這將大大提高性能,因?yàn)樗恍枰P(guān)注程序或節(jié)點(diǎn)樹(shù)的一小部分已更改的內(nèi)容。

          TypeScript 使用不可變的只讀數(shù)據(jù)結(jié)構(gòu),這些數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)在它所稱的 look aside tables 中。這樣很容易知道什么已經(jīng)改變,什么沒(méi)有改變。

          穩(wěn)健性

          在編譯時(shí),有些操作編譯器不確定是安全的,必須等待運(yùn)行時(shí)。每個(gè)編譯器都必須做出困難的選擇,以確定哪些內(nèi)容將被包含,哪些不會(huì)被包含。TypeScript 有一些被稱為不健全的區(qū)域(即需要運(yùn)行時(shí)類型檢查)。

          我們不會(huì)在編譯器中討論上述特性,因?yàn)樗鼈冊(cè)黾恿祟~外的復(fù)雜性,對(duì)于我們的小 POC 來(lái)說(shuō)不值得。

          現(xiàn)在令人興奮的是,我們自己也要實(shí)現(xiàn)一個(gè)編譯器。

          B 部分:構(gòu)建我們自己的類型系統(tǒng)編譯器

          我們將構(gòu)建一個(gè)編譯器,它可以對(duì)三個(gè)不同的場(chǎng)景運(yùn)行類型檢查,并為每個(gè)場(chǎng)景拋出特定的信息。

          我們將其限制在三個(gè)場(chǎng)景中的原因是,我們可以關(guān)注每一個(gè)場(chǎng)景中的具體機(jī)制,并希望到最后能夠?qū)θ绾我敫鼜?fù)雜的類型檢查有一個(gè)更好的構(gòu)思。

          我們將在編譯器中使用函數(shù)聲明和表達(dá)式(調(diào)用該函數(shù))。

          這些場(chǎng)景包括:

          1. 字符串與數(shù)字的類型匹配問(wèn)題

          fn("craig-string"); // throw with string vs number
          function fn(a: number{}

          2. 使用未定義的未知類型

          fn("craig-string"); // throw with string vs ?
          function fn(a: made_up_type{} // throw with bad type

          3. 使用代碼中未定義的屬性名

          interface Person {
            name: string;
          }
          fn({ nam"craig" }); // throw with "nam" vs "name"
          function fn(a: Person{}

          實(shí)現(xiàn)我們的編譯器,需要兩部分:解析器檢查器

          解析器 - Parser

          前面提到,我們今天不會(huì)關(guān)注解析器。我們將遵循 Hegel 的解析方法,假設(shè)一個(gè) typeAnnotation 對(duì)象已經(jīng)附加到所有帶注解的 AST 節(jié)點(diǎn)中。我已經(jīng)硬編碼了 AST 對(duì)象。

          場(chǎng)景 1 將使用以下解析器:

          字符串與數(shù)字的類型匹配問(wèn)題

          function parser(code{
            // fn("craig-string");
            const expressionAst = {
              type"ExpressionStatement",
              expression: {
                type"CallExpression",
                callee: {
                  type"Identifier",
                  name"fn"
                },
                arguments: [
                  {
                    type"StringLiteral"// Parser "Inference" for type.
                    value: "craig-string"
                  }
                ]
              }
            };

            // function fn(a: number) {}
            const declarationAst = {
              type"FunctionDeclaration",
              id: {
                type"Identifier",
                name"fn"
              },
              params: [
                {
                  type"Identifier",
                  name"a",
                  // 參數(shù)標(biāo)識(shí)
                  typeAnnotation: {
                    // our only type annotation
                    type: "TypeAnnotation",
                    typeAnnotation: {
                      // 數(shù)字類型
                      type: "NumberTypeAnnotation"
                    }
                  }
                }
              ],
              body: {
                type"BlockStatement",
                body: [] // "body" === block/line of code. Ours is empty
              }
            };

            const programAst = {
              type"File",
              program: {
                type"Program",
                body: [expressionAst, declarationAst]
              }
            };
            // normal AST except with typeAnnotations on
            return programAst;
          }

          可以看到場(chǎng)景 1 中,第一行 fn("craig-string") 語(yǔ)句的 AST 對(duì)應(yīng) expressionAst,第二行聲明函數(shù)的 AST 對(duì)應(yīng) declarationAst。最后返回一個(gè) programmast,它是一個(gè)包含兩個(gè) AST 塊的程序。

          在AST中,您可以看到參數(shù)標(biāo)識(shí)符 a 上的 typeAnnotation,與它在代碼中的位置相匹配。

          場(chǎng)景 2 將使用以下解析器:

          使用未定義的未知類型

          function parser(code{
            // fn("craig-string");
            const expressionAst = {
              type"ExpressionStatement",
              expression: {
                type"CallExpression",
                callee: {
                  type"Identifier",
                  name"fn"
                },
                arguments: [
                  {
                    type"StringLiteral"// Parser "Inference" for type.
                    value: "craig-string"
                  }
                ]
              }
            };

            // function fn(a: made_up_type) {}
            const declarationAst = {
              type"FunctionDeclaration",
              id: {
                type"Identifier",
                name"fn"
              },
              params: [
                {
                  type"Identifier",
                  name"a",
                  typeAnnotation: {
                    // our only type annotation
                    type: "TypeAnnotation",
                    typeAnnotation: {
                      // 參數(shù)類型不同于場(chǎng)景 1
                      type: "made_up_type" // BREAKS
                    }
                  }
                }
              ],
              body: {
                type"BlockStatement",
                body: [] // "body" === block/line of code. Ours is empty
              }
            };

            const programAst = {
              type"File",
              program: {
                type"Program",
                body: [expressionAst, declarationAst]
              }
            };
            // normal AST except with typeAnnotations on
            return programAst;
          }

          場(chǎng)景 2 的解析器的表達(dá)式、聲明和程序 AST 塊非常類似于場(chǎng)景 1。然而,區(qū)別在于 params 內(nèi)部的 typeAnnotation 是 made_up_type,而不是場(chǎng)景 1 中的 NumberTypeAnnotation

          typeAnnotation: {
            type"made_up_type" // BREAKS
          }

          場(chǎng)景 3 使用以下解析器:

          使用代碼中未定義的屬性名

          function parser(code{
            // interface Person {
            //   name: string;
            // }
            const interfaceAst = {
              type"InterfaceDeclaration",
              id: {
                type"Identifier",
                name"Person",
              },
              body: {
                type"ObjectTypeAnnotation",
                properties: [
                  {
                    type"ObjectTypeProperty",
                    key: {
                      type"Identifier",
                      name"name",
                    },
                    kind"init",
                    methodfalse,
                    value: {
                      type"StringTypeAnnotation",
                    },
                  },
                ],
              },
            };

            // fn({nam: "craig"});
            const expressionAst = {
              type"ExpressionStatement",
              expression: {
                type"CallExpression",
                callee: {
                  type"Identifier",
                  name"fn",
                },
                arguments: [
                  {
                    type"ObjectExpression",
                    properties: [
                      {
                        type"ObjectProperty",
                        methodfalse,
                        key: {
                          type"Identifier",
                          name"nam",
                        },
                        value: {
                          type"StringLiteral",
                          value"craig",
                        },
                      },
                    ],
                  },
                ],
              },
            };

            // function fn(a: Person) {}
            const declarationAst = {
              type"FunctionDeclaration",
              id: {
                type"Identifier",
                name"fn",
              },
              params: [
                {
                  type"Identifier",
                  name"a",
                  // 
                  typeAnnotation: {
                    type"TypeAnnotation",
                    typeAnnotation: {
                      type"GenericTypeAnnotation",
                      id: {
                        type"Identifier",
                        name"Person",
                      },
                    },
                  },
                },
              ],
              body: {
                type"BlockStatement",
                body: [], // Empty function
              },
            };

            const programAst = {
              type"File",
              program: {
                type"Program",
                body: [interfaceAst, expressionAst, declarationAst],
              },
            };
            // normal AST except with typeAnnotations on
            return programAst;
          }

          除了表達(dá)式、聲明和程序 AST 塊之外,還有一個(gè) interfaceAst 塊,它負(fù)責(zé)保存 InterfaceDeclaration AST。

          declarationAst 塊的 typeAnnotation 節(jié)點(diǎn)上有一個(gè) GenericType,因?yàn)樗邮芤粋€(gè)對(duì)象標(biāo)識(shí)符,即 Person。在這個(gè)場(chǎng)景中,programAst 將返回這三個(gè)對(duì)象的數(shù)組。

          解析器的相似性

          從上面可以得知,這三種有共同點(diǎn), 3 個(gè)場(chǎng)景中保存所有的類型注解的主要區(qū)域是 declaration

          檢查器

          現(xiàn)在來(lái)看編譯器的類型檢查部分。

          它需要遍歷所有程序主體的 AST 對(duì)象,并根據(jù)節(jié)點(diǎn)類型進(jìn)行適當(dāng)?shù)念愋蜋z查。我們將把所有錯(cuò)誤添加到一個(gè)數(shù)組中,并返回給調(diào)用者以便打印。

          在我們進(jìn)一步討論之前,對(duì)于每種類型,我們將使用的基本邏輯是:

          • 函數(shù)聲明:檢查參數(shù)的類型是否有效,然后檢查函數(shù)體中的每個(gè)語(yǔ)句。

          • 表達(dá)式:找到被調(diào)用的函數(shù)聲明,獲取聲明上的參數(shù)類型,然后獲取函數(shù)調(diào)用表達(dá)式傳入的參數(shù)類型,并進(jìn)行比較。

          代碼

          以下代碼中包含 typeChecks 對(duì)象(和 errors 數(shù)組) ,它將用于表達(dá)式檢查和基本的注解(annotation)檢查。

          const errors = [];

          // 注解類型
          const ANNOTATED_TYPES = {
            NumberTypeAnnotation"number",
            GenericTypeAnnotationtrue
          };

          // 類型檢查的邏輯
          const typeChecks = {
            // 比較形參和實(shí)參的類型
            expression: (declarationFullType, callerFullArg) => {
              switch (declarationFullType.typeAnnotation.type) {
                // 注解為 number 類型
                case "NumberTypeAnnotation":
                  // 如果調(diào)用時(shí)傳入的是數(shù)字,返回 true
                  return callerFullArg.type === "NumericLiteral";
                // 注解為通用類型
                case "GenericTypeAnnotation"// non-native
                  // 如果是對(duì)象,檢查對(duì)象的屬性
                  if (callerFullArg.type === "ObjectExpression") {
                    // 獲取接口節(jié)點(diǎn)
                    const interfaceNode = ast.program.body.find(
                      node => node.type === "InterfaceDeclaration"
                    );
                    const properties = interfaceNode.body.properties;

                    //遍歷檢查調(diào)用時(shí)的每個(gè)屬性
                    properties.map((prop, index) => {
                      const name = prop.key.name;
                      const associatedName = callerFullArg.properties[index].key.name;
                      // 沒(méi)有匹配,將錯(cuò)誤信息存入 errors
                      if (name !== associatedName) {
                        errors.push(
                          `Property "${associatedName}" does not exist on interface "${interfaceNode.id.name}". Did you mean Property "${name}"?`
                        );
                      }
                    });
                  }
                  return true// as already logged
              }
            },
            annotationCheckarg => {
              return !!ANNOTATED_TYPES[arg];
            }
          };

          讓我們來(lái)看一下代碼,我們的 expression 有兩種類型的檢查:

          • 對(duì)于 NumberTypeAnnotation; 調(diào)用時(shí)類型應(yīng)為 AnumericTeral(即,如果注解為數(shù)字,則調(diào)用時(shí)類型應(yīng)為數(shù)字)。場(chǎng)景 1 將在此處失敗,但未記錄任何錯(cuò)誤信息。

          • 對(duì)于 GenericTypeAnnotation; 如果是一個(gè)對(duì)象,我們將在 AST 中查找 InterfaceDeclaration 節(jié)點(diǎn),然后檢查該接口上調(diào)用者的每個(gè)屬性。之后將所有錯(cuò)誤信息都會(huì)被存到 errors 數(shù)組中,場(chǎng)景 3 將在這里失敗并得到這個(gè)錯(cuò)誤。

          我們的處理僅限于這個(gè)文件中,大多數(shù)類型檢查器都有作用域的概念,因此它們能夠確定聲明在運(yùn)行時(shí)的準(zhǔn)確位置。我們的工作更簡(jiǎn)單,因?yàn)樗皇且粋€(gè) POC

          以下代碼包含程序體中每個(gè)節(jié)點(diǎn)類型的處理。這就是上面調(diào)用類型檢查邏輯的地方。

          // Process program
          ast.program.body.map(stnmt => {
            switch (stnmt.type) {
              case "FunctionDeclaration":
                stnmt.params.map(arg => {
                  // Does arg has a type annotation?
                  if (arg.typeAnnotation) {
                    const argType = arg.typeAnnotation.typeAnnotation.type;
                    // Is type annotation valid
                    const isValid = typeChecks.annotationCheck(argType);
                    if (!isValid) {
                      errors.push(
                        `Type "${argType}" for argument "${arg.name}" does not exist`
                      );
                    }
                  }
                });

                // Process function "block" code here
                stnmt.body.body.map(line => {
                  // Ours has none
                });

                return;
              case "ExpressionStatement":
                const functionCalled = stnmt.expression.callee.name;
                const declationForName = ast.program.body.find(
                  node =>
                    node.type === "FunctionDeclaration" &&
                    node.id.name === functionCalled
                );

                // Get declaration
                if (!declationForName) {
                  errors.push(`Function "${functionCalled}" does not exist`);
                  return;
                }

                // Array of arg-to-type. e.g. 0 = NumberTypeAnnotation
                const argTypeMap = declationForName.params.map(param => {
                  if (param.typeAnnotation) {
                    return param.typeAnnotation;
                  }
                });

                // Check exp caller "arg type" with declaration "arg type"
                stnmt.expression.arguments.map((arg, index) => {
                  const declarationType = argTypeMap[index].typeAnnotation.type;
                  const callerType = arg.type;
                  const callerValue = arg.value;

                  // Declaration annotation more important here
                  const isValid = typeChecks.expression(
                    argTypeMap[index], // declaration details
                    arg // caller details
                  );

                  if (!isValid) {
                    const annotatedType = ANNOTATED_TYPES[declarationType];
                    // Show values to user, more explanatory than types
                    errors.push(
                      `Type "${callerValue}" is incompatible with "${annotatedType}"`
                    );
                  }
                });

                return;
            }
          });

          讓我們?cè)俅伪闅v代碼,按類型對(duì)其進(jìn)行分解。

          FunctionDeclaration (即 function hello(){})

          首先處理 arguments/params。如果找到類型注解,就檢查給定參數(shù)的類型 argType 是否存在。如果不進(jìn)行錯(cuò)誤處理,場(chǎng)景 2 會(huì)在這里報(bào)錯(cuò)誤。

          之后處理函數(shù)體,但是我們知道沒(méi)有函數(shù)體需要處理,所以我把它留空了。

          stnmt.body.body.map(line => {
            // Ours has none
          });

          ExpressionStatement (即 hello())

          首先檢查程序中函數(shù)的聲明。這就是作用域?qū)?yīng)用于實(shí)際類型檢查器的地方。如果找不到聲明,就將錯(cuò)誤信息添加到 errors 數(shù)組中。

          接下來(lái),我們針對(duì)調(diào)用時(shí)傳入的參數(shù)類型(實(shí)參類型)檢查每個(gè)已定義的參數(shù)類型。如果發(fā)現(xiàn)類型不匹配,則向 errors 數(shù)組中添加一個(gè)錯(cuò)誤。場(chǎng)景 1 和場(chǎng)景 2 在這里都會(huì)報(bào)錯(cuò)。

          運(yùn)行我們的編譯器

          源碼存放在這里,該文件一次性處理所有三個(gè) AST 節(jié)點(diǎn)對(duì)象并記錄錯(cuò)誤。

          運(yùn)行它時(shí),我得到以下信息:

          總而言之:

          場(chǎng)景 1:

          fn("craig-string"); // throw with string vs number
          function fn(a: number{}

          我們定義參數(shù)為 number 的類型,然后用字符串調(diào)用它。

          場(chǎng)景 2:

          fn("craig-string"); // throw with string vs ?
          function fn(a: made_up_type{} // throw with bad type

          我們?cè)诤瘮?shù)參數(shù)上定義了一個(gè)不存在的類型,然后調(diào)用我們的函數(shù),所以我們得到了兩個(gè)錯(cuò)誤(一個(gè)是定義的錯(cuò)誤類型,另一個(gè)是類型不匹配的錯(cuò)誤)。

          場(chǎng)景 3:

          interface Person {
            name: string;
          }
          fn({ nam"craig" }); // throw with "nam" vs "name"
          function fn(a: Person{}

          我們定義了一個(gè)接口,但是使用了一個(gè)名為 nam 的屬性,這個(gè)屬性不在對(duì)象上,錯(cuò)誤提示我們是否要使用 name

          我們遺漏了什么?

          如前所述,類型編譯器還有許多其他部分,我們?cè)诰幾g器中省略了這些部分。其中包括:

          • 解析器:我們是手動(dòng)編寫的 AST 代碼,它們實(shí)際上是在類型的編譯器上解析生成。
          • 預(yù)處理/語(yǔ)言編譯器: 一個(gè)真正的編譯器具有插入 IDE 并在適當(dāng)?shù)臅r(shí)候重新運(yùn)行的機(jī)制。
          • 懶編譯:沒(méi)有關(guān)于更改或內(nèi)存使用的信息。
          • 轉(zhuǎn)換:我們跳過(guò)了編譯器的最后一部分,也就是生成本機(jī) JavaScript 代碼的地方。
          • 作用域:因?yàn)槲覀兊?POC 是一個(gè)單一的文件,它不需要理解作用域的概念,但是真正的編譯器必須始終知道上下文。

          非常感謝您的閱讀和觀看,我從這項(xiàng)研究中了解了大量關(guān)于類型系統(tǒng)的知識(shí),希望對(duì)您有所幫助。以上完整代碼您可以在這里找到。(給原作者 start)

          備注:

          原作者在源碼中使用的 Node 模塊方式為 ESM(ES Module),在將源碼克隆到本地后,如果運(yùn)行不成功,需要修改 start 指令,添加啟動(dòng)參數(shù) --experimental-modules

          "start""node --experimental-modules src/index.mjs",

          END



          如果覺(jué)得這篇文章還不錯(cuò)
          點(diǎn)擊下面卡片關(guān)注我
          來(lái)個(gè)【分享、點(diǎn)贊、在看】三連支持一下吧

             “分享、點(diǎn)贊在看” 支持一波  

          瀏覽 48
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本一区二区视频在线 | 亚洲无码免费的 | 欧美综合网站 | 五月天婷婷乱伦 | 伊人丁香网|