Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import literal "module"#export hoists import { export } from "module" #1652

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions source/parser.hera
Original file line number Diff line number Diff line change
Expand Up @@ -985,7 +985,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
Expand Down Expand Up @@ -5866,10 +5868,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
Expand Down Expand Up @@ -6367,6 +6370,23 @@ TemplateBlockCharacters
/(?:\$(?!\{)|`(?!``)|\\.|[^$`])+/ ->
return { $loc, token: $0 }

# ^x^y and "x"^y result in `import { y } from x` at top of file
ImportLiteral
Caret UnprocessedModuleSpecifier:module Caret ( Star / ModuleExportName )?:exp ->
return {
type: "ImportLiteral",
children: [module, exp],
module,
export: exp || "default",
}
StringLiteral:module Caret ( Star / ModuleExportName )?:exp ->
return {
type: "ImportLiteral",
children: [module, exp],
module,
export: exp || "default",
}

# https://262.ecma-international.org/#sec-comments
ReservedWord
/(?:on|off|yes|no)(?!\p{ID_Continue})/ CoffeeBooleansEnabled
Expand Down
83 changes: 83 additions & 0 deletions source/parser/lib.civet
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import type {
ForStatement
FunctionSignature
IfStatement
ImportDeclaration
ImportLiteral
Initializer
IterationStatement
Label
Expand Down Expand Up @@ -87,8 +89,10 @@ import {
prepend
replaceNode
replaceNodes
stringLiteralValue
stripTrailingImplicitComma
trimFirstSpace
updateParentPointers
wrapIIFE
wrapWithReturn
} from ./util.civet
Expand Down Expand Up @@ -1464,6 +1468,84 @@ 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<string, ImportRecord>
}
importMap := new Map<string, ImportDeclarationWithMap>

for each literal of gatherRecursiveAll root, .type is "ImportLiteral"
module .= stringLiteralValue literal.module
exp := literal.export
exportName :=
switch exp
<? "string"
exp
{type: "Identifier", name}
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<string, ImportRecord>
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}`
.replace /^[^\p{ID_Start}_$]+|[^\p{ID_Continue}_$]/ug, ''
name or= "imp"
ref = makeRef name
map.set exportName, {export: exp, 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

literal.children =
// leading whitespace
. ...literal.children[< literal.children.indexOf literal.module]
. ref

root.expressions.unshift ...for imp of importMap.values()
// @ts-ignore Transform into normal ImportDeclaration
delete imp.map
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

function processCoffeeClasses(statements: StatementTuple[]): void
for each ce of gatherRecursiveAll statements, .type is "ClassExpression"
{ expressions } := ce.body
Expand Down Expand Up @@ -1571,6 +1653,7 @@ function processProgram(root: BlockStatement): void
processIterationExpressions(statements)
processFinallyClauses(statements)
processBreaksContinues(statements)
processImportLiterals(root)

// Hoist hoistDec attributes to actual declarations.
// NOTE: This should come after iteration expressions get processed
Expand Down
16 changes: 15 additions & 1 deletion source/parser/types.civet
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type ExpressionNode =
| FunctionNode
| Identifier
| IfExpression
| ImportLiteral
| IterationExpression
| Literal
| MethodDefinition
Expand Down Expand Up @@ -112,6 +113,7 @@ export type OtherNode =
| ModuleSpecifier
| NonNullAssertion
| NormalCatchParameter
| NumericLiteral
| ObjectBindingPattern
| Parameter
| ParametersNode
Expand All @@ -128,6 +130,7 @@ export type OtherNode =
| ReturnValue
| SliceExpression
| SpreadElement
| StringLiteral
| ThisType
| TypeArgument
| TypeArguments
Expand Down Expand Up @@ -182,7 +185,7 @@ export type ASTLeaf =
children?: never

export type ASTLeafWithType<T extends string> =
Exclude<ASTLeaf, "type"> & { type: T }
Omit<ASTLeaf, "type"> & { type: T }

export type CommentNode =
type: "Comment"
Expand Down Expand Up @@ -1144,6 +1147,17 @@ 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
| (ASTLeaf & { token: "*" })

export type RangeExpression
type: "RangeExpression"
children: Children
Expand Down
20 changes: 14 additions & 6 deletions source/parser/util.civet
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
RegularExpressionLiteral
ReturnTypeAnnotation
StatementNode
StringLiteral
StatementTuple
TemplateLiteral
TypeNode
Expand Down Expand Up @@ -380,12 +381,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")
Expand All @@ -399,8 +395,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 <? "string"
assert.equal
(or)
string.startsWith('"') and string.endsWith('"')
string.startsWith("'") and string.endsWith("'")
true, "String literal should begin and end in single or double quotes"
string[1...-1]

/** TypeScript type for given literal */
function literalType(literal: Literal | RegularExpressionLiteral | TemplateLiteral): TypeNode
let t: ASTString
Expand Down Expand Up @@ -938,6 +945,7 @@ export {
spliceChild
startsWith
startsWithPredicate
stringLiteralValue
stripTrailingImplicitComma
trimFirstSpace
updateParentPointers
Expand Down
62 changes: 62 additions & 0 deletions test/import.civet
Original file line number Diff line number Diff line change
Expand Up @@ -557,3 +557,65 @@ describe "import", ->
---
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)
"""

testCase """
default
---
^node:fs^.readFile filename
---
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
"""
Loading