From b0ad36757c43a24c8342b262022bddb8f6cd4619 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Thu, 19 Dec 2024 16:50:03 -0500 Subject: [PATCH 1/2] Import literal `module#export` hoists `import { export } from module` Fixes #113 --- source/parser.hera | 12 ++++++++ source/parser/lib.civet | 64 +++++++++++++++++++++++++++++++++++++++ source/parser/types.civet | 12 +++++++- source/parser/util.civet | 20 ++++++++---- test/import.civet | 40 ++++++++++++++++++++++++ 5 files changed, 141 insertions(+), 7 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index 2c18f3d9..e83a893a 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -935,7 +935,9 @@ PrimaryExpression # NOTE: ObjectLiteral needs to be before ArrayLiteral to support `[x]: y` ArrayLiteral ThisLiteral + ImportLiteral TemplateLiteral + # NOTE: ImportLiteral must be before Literal as it starts with StringLiteral # NOTE: TemplateLiteral must be before Literal, so that CoffeeScript # interpolated strings get checked first before StringLiteral. Literal @@ -6298,6 +6300,16 @@ TemplateBlockCharacters /(?:\$(?!\{)|`(?!``)|\\.|[^$`])+/ -> return { $loc, token: $0 } +# "x"#y results in `import { y } from x` at top of file +ImportLiteral + StringLiteral:module Hash ModuleExportName:exp -> + return { + type: "ImportLiteral", + children: $0, + module, + export: exp, + } + # https://262.ecma-international.org/#sec-comments ReservedWord /(?:on|off|yes|no)(?!\p{ID_Continue})/ CoffeeBooleansEnabled diff --git a/source/parser/lib.civet b/source/parser/lib.civet index 1b576cbe..b73b520a 100644 --- a/source/parser/lib.civet +++ b/source/parser/lib.civet @@ -29,6 +29,8 @@ import type { ForStatement FunctionSignature IfStatement + ImportDeclaration + ImportLiteral Initializer IterationStatement Label @@ -42,6 +44,7 @@ import type { Placeholder StatementNode StatementTuple + StringLiteral SwitchStatement TypeArgument TypeArguments @@ -87,8 +90,10 @@ import { prepend replaceNode replaceNodes + stringLiteralValue stripTrailingImplicitComma trimFirstSpace + updateParentPointers wrapIIFE wrapWithReturn } from ./util.civet @@ -1464,6 +1469,64 @@ function processBreaksContinues(statements: StatementTuple[]): void label.children.push label.name = parent.label.name delete label.special +function processImportLiterals(root: BlockStatement): void + type ImportRecord = { export: ImportLiteral["export"], ref: ASTRef } + type ImportDeclarationWithMap = ImportDeclaration & { + imports: ASTNode[][] + map: Map + } + importMap := new Map + + for each literal of gatherRecursiveAll root, .type is "ImportLiteral" + module := stringLiteralValue literal.module + let imports: ASTNode[][], map + unless importMap.has module + from := [ "from ", literal.module ] + map = new Map + imports = [] + importMap.set module, {} + type: "ImportDeclaration" + children: [ "import { ", imports, " } ", from ] + imports + map + from + else + { imports, map } = importMap.get(module)! + + exp := literal.export + exportName := + switch exp + = - Exclude & { type: T } + Omit & { type: T } export type CommentNode = type: "Comment" @@ -1136,6 +1139,13 @@ export type LiteralContentNode = export type NumericLiteral = ASTLeafWithType "NumericLiteral" export type StringLiteral = ASTLeafWithType "StringLiteral" +export type ImportLiteral = + type: "ImportLiteral" + children: Children + parent?: Parent + module: StringLiteral + export: Identifier | StringLiteral | ASTString + export type RangeExpression type: "RangeExpression" children: Children diff --git a/source/parser/util.civet b/source/parser/util.civet index 883cca7d..ea12c192 100644 --- a/source/parser/util.civet +++ b/source/parser/util.civet @@ -15,6 +15,7 @@ import type { ParametersNode Parent StatementNode + StringLiteral TypeSuffix ReturnTypeAnnotation StatementTuple @@ -376,12 +377,7 @@ function literalValue(literal: Literal) case "false": return false switch literal.subtype when "StringLiteral" - assert.equal - (or) - raw.startsWith('"') and raw.endsWith('"') - raw.startsWith("'") and raw.endsWith("'") - true, "String literal should begin and end in single or double quotes" - return raw[1...-1] + return stringLiteralValue raw when "NumericLiteral" raw = raw.replace(/_/g, "") if raw.endsWith("n") @@ -395,8 +391,19 @@ function literalValue(literal: Literal) case "o": return parseInt(raw.replace(/0[oO]/, ""), 8) return parseInt(raw, 10) else + delete literal.parent throw new Error("Unrecognized literal " + JSON.stringify(literal)) +// TODO: handle escape sequences +function stringLiteralValue(string: string | StringLiteral): string + string = string.token unless string --- ParseError """ + +describe "import literals", -> + testCase """ + basic + --- + "node:fs"#readFile filename + --- + import { readFile as nodefs$readFile } from "node:fs"; + nodefs$readFile(filename) + """ + + testCase """ + multiple + --- + "node:fs"#readFile filename + 'node:fs'#writeFile filename + 'fs/promises'#readFile filename + "fs/promises"#writeFile filename + "node:fs"#readFile filename + 'node:fs'#writeFile filename + --- + import { readFile as nodefs$readFile, writeFile as nodefs$writeFile } from "node:fs"; + import { readFile as fspromises$readFile, writeFile as fspromises$writeFile } from 'fs/promises'; + nodefs$readFile(filename) + nodefs$writeFile(filename) + fspromises$readFile(filename) + fspromises$writeFile(filename) + nodefs$readFile(filename) + nodefs$writeFile(filename) + """ + + // This desirable shorthand conflicts with length shorthand + testCase.skip """ + default + --- + "node:fs"#.readFile filename + --- + import { default as nodefs$default } from "node:fs"; + nodefs$default.readFile(filename) + """ From 13b6a37d64cf4100c1e0e1f3fe68aeb26f7203dd Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Tue, 24 Dec 2024 09:15:36 -0500 Subject: [PATCH 2/2] Import literal `^module^export` or `"module"^export` --- source/parser.hera | 20 +++++++++----- source/parser/lib.civet | 56 ++++++++++++++++++++++++++------------- source/parser/types.civet | 8 ++++-- test/import.civet | 44 ++++++++++++++++++++++-------- 4 files changed, 91 insertions(+), 37 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index e83a893a..137d94b7 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -5803,10 +5803,11 @@ UnprocessedModuleSpecifier UnquotedSpecifier # Currently allowing most non-whitespace characters - # Forbidding ; (statement separator), = (assignment), > (function arrow) + # Forbidding ; (statement separator), = (assignment), > (function arrow), + # ^ (import literal) # It may make sense to restrict this to only allow characters that are valid in a module specifier # Also consider URLs - /[^;"\s=>]+/:spec -> + /[^;"\s=>^]+/:spec -> return { $loc, token: `"${spec}"` } # https://262.ecma-international.org/#prod-ImportedBinding @@ -6300,14 +6301,21 @@ TemplateBlockCharacters /(?:\$(?!\{)|`(?!``)|\\.|[^$`])+/ -> return { $loc, token: $0 } -# "x"#y results in `import { y } from x` at top of file +# ^x^y and "x"^y result in `import { y } from x` at top of file ImportLiteral - StringLiteral:module Hash ModuleExportName:exp -> + Caret UnprocessedModuleSpecifier:module Caret ( Star / ModuleExportName )?:exp -> return { type: "ImportLiteral", - children: $0, + children: [module, exp], + module, + export: exp || "default", + } + StringLiteral:module Caret ( Star / ModuleExportName )?:exp -> + return { + type: "ImportLiteral", + children: [module, exp], module, - export: exp, + export: exp || "default", } # https://262.ecma-international.org/#sec-comments diff --git a/source/parser/lib.civet b/source/parser/lib.civet index b73b520a..e9136755 100644 --- a/source/parser/lib.civet +++ b/source/parser/lib.civet @@ -1472,27 +1472,13 @@ function processBreaksContinues(statements: StatementTuple[]): void function processImportLiterals(root: BlockStatement): void type ImportRecord = { export: ImportLiteral["export"], ref: ASTRef } type ImportDeclarationWithMap = ImportDeclaration & { - imports: ASTNode[][] + imports: ASTNode[] map: Map } importMap := new Map for each literal of gatherRecursiveAll root, .type is "ImportLiteral" - module := stringLiteralValue literal.module - let imports: ASTNode[][], map - unless importMap.has module - from := [ "from ", literal.module ] - map = new Map - imports = [] - importMap.set module, {} - type: "ImportDeclaration" - children: [ "import { ", imports, " } ", from ] - imports - map - from - else - { imports, map } = importMap.get(module)! - + module .= stringLiteralValue literal.module exp := literal.export exportName := switch exp @@ -1502,8 +1488,29 @@ function processImportLiterals(root: BlockStatement): void name {type: "StringLiteral"} stringLiteralValue exp + {token: "*"} + "*" else throw new Error `Invalid export name ${exp} in import literal` + // Use separate import declaration for * import because JS does not support + // combination of default import, * import, and named imports + if exportName is "*" + module += "*" + + let imports: ASTNode[], map + unless importMap.has module + from := [ "from ", literal.module ] + map = new Map + imports = ["{ "] + importMap.set module, {} + type: "ImportDeclaration" + children: [ "import ", imports, " ", from ] + imports + map + from + else + { imports, map } = importMap.get(module)! + let ref unless map.has exportName name .= `${module}$${exportName}` @@ -1511,7 +1518,14 @@ function processImportLiterals(root: BlockStatement): void name or= "imp" ref = makeRef name map.set exportName, {export: exp, ref} - imports.push [ exp, " as ", ref, ", " ] + if exportName is "default" + imports.unshift ref, ", " + else + importAs := [ exp, " as ", ref, ", " ] + if exportName is "*" + imports.unshift ...importAs + else + imports.push ...importAs else ref = map.get(exportName)!.ref @@ -1523,7 +1537,13 @@ function processImportLiterals(root: BlockStatement): void root.expressions.unshift ...for imp of importMap.values() // @ts-ignore Transform into normal ImportDeclaration delete imp.map - imp.imports.-1?.pop() // remove trailing comma + named := imp.imports.-1 is not "{ " // any named imports? + unless named + imp.imports.pop() // remove unneeded open brace + if imp.imports.-1 is ", " + imp.imports.pop() // remove trailing comma + if named + imp.imports.push " }" updateParentPointers imp, root ["", imp, ";\n"] as tuple diff --git a/source/parser/types.civet b/source/parser/types.civet index 294d525a..6f131322 100644 --- a/source/parser/types.civet +++ b/source/parser/types.civet @@ -1139,12 +1139,16 @@ export type LiteralContentNode = export type NumericLiteral = ASTLeafWithType "NumericLiteral" export type StringLiteral = ASTLeafWithType "StringLiteral" -export type ImportLiteral = +export type ImportLiteral type: "ImportLiteral" children: Children parent?: Parent module: StringLiteral - export: Identifier | StringLiteral | ASTString + export: + | Identifier + | StringLiteral + | ASTString + | (ASTLeaf & { token: "*" }) export type RangeExpression type: "RangeExpression" diff --git a/test/import.civet b/test/import.civet index d8eabdcd..cc72038c 100644 --- a/test/import.civet +++ b/test/import.civet @@ -562,7 +562,7 @@ describe "import literals", -> testCase """ basic --- - "node:fs"#readFile filename + ^node:fs^readFile filename --- import { readFile as nodefs$readFile } from "node:fs"; nodefs$readFile(filename) @@ -571,12 +571,12 @@ describe "import literals", -> testCase """ multiple --- - "node:fs"#readFile filename - 'node:fs'#writeFile filename - 'fs/promises'#readFile filename - "fs/promises"#writeFile filename - "node:fs"#readFile filename - 'node:fs'#writeFile filename + "node:fs"^readFile filename + 'node:fs'^writeFile filename + 'fs/promises'^readFile filename + "fs/promises"^writeFile filename + "node:fs"^readFile filename + 'node:fs'^writeFile filename --- import { readFile as nodefs$readFile, writeFile as nodefs$writeFile } from "node:fs"; import { readFile as fspromises$readFile, writeFile as fspromises$writeFile } from 'fs/promises'; @@ -588,12 +588,34 @@ describe "import literals", -> nodefs$writeFile(filename) """ - // This desirable shorthand conflicts with length shorthand - testCase.skip """ + testCase """ default --- - "node:fs"#.readFile filename + ^node:fs^.readFile filename --- - import { default as nodefs$default } from "node:fs"; + import nodefs$default from "node:fs"; nodefs$default.readFile(filename) """ + + testCase """ + star + --- + ^node:fs^*.readFile filename + --- + import * as nodefs$ from "node:fs"; + nodefs$.readFile(filename) + """ + + testCase """ + all + --- + def := ^node:fs^ + star := ^node:fs^* + readFile := ^node:fs^readFile + --- + import nodefs$default, { readFile as nodefs$readFile } from "node:fs"; + import * as nodefs$ from "node:fs"; + const def = nodefs$default + const star = nodefs$ + const readFile = nodefs$readFile + """