Skip to content

Commit

Permalink
Merge pull request #1650 from DanielXMoore/coffee-classes
Browse files Browse the repository at this point in the history
`coffeeClasses` improved compatibility: private static class fields via `=`, bound methods via `=>` , `constructor` shouldn't `return`
  • Loading branch information
edemaine authored Dec 19, 2024
2 parents 6643a81 + 9dc6364 commit a1ed02f
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 34 deletions.
4 changes: 3 additions & 1 deletion civet.dev/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -3359,10 +3359,12 @@ loop
### CoffeeScript Classes
<Playground>
"civet coffeeClasses"
"civet coffeeClasses autoVar"
class X
privateVar = 5
constructor: (@x) ->
get: -> @x
bound: => @x
</Playground>
### IIFE Wrapper
Expand Down
50 changes: 43 additions & 7 deletions source/parser.hera
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
attachPostfixStatementAsExpression,
append,
blockWithPrefix,
braceBlock,
convertNamedImportsToObject,
convertObjectToJSXAttributes,
convertWithClause,
Expand Down Expand Up @@ -1244,12 +1245,11 @@ AccessModifier

# https://262.ecma-international.org/#prod-FieldDefinition
FieldDefinition
# TODO: CoffeeCompat class method fields
# name: (param1, param2) ->
# name: (param1, param2) => which gets wrapped in bind in constructor
CoffeeClassesEnabled ClassElementName:id _? Colon __ AssignmentExpression:exp ->
switch (exp.type) {
// TODO: => functions
case "FunctionExpression":
case "FunctionExpression": {
const fnTokenIndex = exp.children.findIndex(c => c?.token?.startsWith("function"))
// copy
const children = exp.children.slice()
Expand All @@ -1262,8 +1262,31 @@ FieldDefinition
}
return {
...exp,
type: "MethodDefinition",
name: id.name,
signature: { ...exp.signature, id, name: id.name },
children,
}
}
case "ArrowFunction": {
const block = {...exp.block} // prepare for bracing
const children = exp.children
.filter(c => !(Array.isArray(c) && c[c.length-1]?.token?.includes("=>")))
.map(c => c === exp.block ? block : c)
children.unshift(id)
exp = {
...exp,
type: "MethodDefinition",
name: id.name,
signature: { ...exp.signature, id, name: id.name },
block,
children,
autoBind: true
}
block.parent = exp // needed by braceBlock
braceBlock(block)
return exp
}
default:
return {
type: "FieldDefinition",
Expand All @@ -1273,9 +1296,9 @@ FieldDefinition
}

# NOTE: Added readonly semantic equivalent of const field assignment
InsertReadonly:r ClassElementName:id TypeSuffix?:typeSuffix __ ConstAssignment:ca MaybeNestedExpression ->
InsertReadonly:readonly ClassElementName:id TypeSuffix?:typeSuffix __ ConstAssignment:ca MaybeNestedExpression ->
// Adjust position to space before assignment to make TypeScript remapping happier
r.children[0].$loc = {
readonly.children[0].$loc = {
pos: ca.$loc.pos - 1,
length: ca.$loc.length + 1,
}
Expand All @@ -1284,15 +1307,28 @@ FieldDefinition
id,
typeSuffix,
children: $0,
readonly,
}

( Abstract _? )? ( Readonly _? )? ClassElementName:id TypeSuffix?:typeSuffix Initializer? ->
# In a CoffeeScript class, `x = y` makes a private variable x visible
# only within the class scope only
CoffeeClassesEnabled ActualAssignment:assignment ->
return {
type: "CoffeeClassPrivate",
children: [ assignment ],
assignment,
}

( Abstract _? )?:abstract ( Readonly _? )?:readonly ClassElementName:id TypeSuffix?:typeSuffix Initializer?:initializer ->
return {
type: "FieldDefinition",
children: $0,
ts: $1 ? true : undefined,
ts: abstract ? true : undefined,
id,
typeSuffix,
abstract,
readonly,
initializer,
}

ThisLiteral
Expand Down
9 changes: 3 additions & 6 deletions source/parser/auto-dec.civet
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,9 @@ function createVarDecs(block: BlockStatement, scopes, pushVar?): void
assignmentStatements

// Let descendent blocks add the var at the outer enclosing function scope
if (!pushVar) {
pushVar = function (name) {
varIds.push(name)
decs.add(name)
}
}
pushVar ?= (name: string) =>
varIds.push(name)
decs.add(name)

{ expressions: statements } := block
decs := findDecs statements
Expand Down
28 changes: 18 additions & 10 deletions source/parser/function.civet
Original file line number Diff line number Diff line change
Expand Up @@ -1053,20 +1053,27 @@ function processParams(f: FunctionNode): void

return unless prefix#
// In constructor definition, insert prefix after first super() call
index .= -1
if isConstructor
superCalls := gatherNodes expressions,
(is like {type: "CallExpression", children: [ {token: "super"}, ... ]}) as Predicate<CallExpression>
if superCalls#
{child} := findAncestor superCalls[0], (is block)
index := findChildIndex expressions, child
if index < 0
throw new Error("Could not find super call within top-level expressions")
expressions.splice(index + 1, 0, ...prefix)
return
expressions.unshift(...prefix)
index = findSuperCall block
expressions.splice(index + 1, 0, ...prefix)
updateParentPointers block
braceBlock block

/** Returns index of (first) super call expression in block, if there's one */
function findSuperCall(block: BlockStatement): number
{ expressions } := block
superCalls := gatherNodes expressions,
(is like {type: "CallExpression", children: [ {token: "super"}, ... ]}) as Predicate<CallExpression>
if superCalls#
{child} := findAncestor superCalls[0], (is block)
index := findChildIndex expressions, child
if index < 0
throw new Error("Could not find super call within top-level expressions")
index
else
-1

function processSignature(f: FunctionNode): void
{block, signature} := f

Expand Down Expand Up @@ -1294,6 +1301,7 @@ function makeAmpersandFunction(rhs: AmpersandBlockBody): ASTNode

export {
assignResults
findSuperCall
insertReturn
makeAmpersandFunction
processCoffeeDo
Expand Down
82 changes: 78 additions & 4 deletions source/parser/lib.civet
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type {
ElseClause
FinallyClause
ForStatement
FunctionSignature
IfStatement
Initializer
IterationStatement
Expand Down Expand Up @@ -103,6 +104,7 @@ import {
import {
blockContainingStatement
blockWithPrefix
braceBlock
duplicateBlock
hoistRefDecs
makeBlockFragment
Expand All @@ -123,6 +125,7 @@ import {
import { processPipelineExpressions } from ./pipe.civet
import { forRange, processForInOf, processRangeExpression } from ./for.civet
import {
findSuperCall
makeAmpersandFunction
processCoffeeDo
processFunctions
Expand Down Expand Up @@ -1461,6 +1464,75 @@ function processBreaksContinues(statements: StatementTuple[]): void
label.children.push label.name = parent.label.name
delete label.special

function processCoffeeClasses(statements: StatementTuple[]): void
for each ce of gatherRecursiveAll statements, .type is "ClassExpression"
{ expressions } := ce.body
indent := expressions[0]?[0] ?? '\n'

autoBinds := expressions.filter (&[1] as MethodDefinition?)?.autoBind
if autoBinds#
let construct: MethodDefinition?
for [, c] of expressions
if c is like {type: "MethodDefinition", name: "constructor"} and c.block
construct = c
break
unless construct
parametersList: never[] := []
parameters: ParametersNode :=
type: "Parameters"
children: [parametersList]
parameters: parametersList
names: []
signature: FunctionSignature := {}
type: "MethodSignature"
children: [ "constructor(", parameters, ")" ]
parameters
modifier: {}
returnType: undefined
block := makeEmptyBlock()
construct = {}
...signature
type: "MethodDefinition"
name: "constructor"
block
signature
children: [ ...signature.children, block ]
expressions.unshift [indent, construct]
index := findSuperCall construct.block
construct.block.expressions.splice index+1, 0,
...for each [, a] of autoBinds
[indent, ["this.", a.name, " = this.", a.name, ".bind(this)"], ";"]

// In a CoffeeScript class, `x = y` makes a private variable x visible
// only within the class scope only, implemented via IIFE wrapper
privates := expressions.filter &[1]?.type is "CoffeeClassPrivate"
continue unless privates#

{ parent } := ce

// Remove privates from class body, in place
for i of [expressions# >.. 0]
if expressions[i][1]?.type is "CoffeeClassPrivate"
expressions.splice i, 1

wrapped .= wrapIIFE
. ...privates
. [indent, wrapWithReturn ce]

// Outer assignment
if { binding } .= ce
binding = trimFirstSpace binding
wrapped = makeNode {
type: "AssignmentExpression"
children: [binding, " = ", wrapped]
lhs: binding as any // TODO: incorrect shape
assigned: binding
expression: wrapped as CallExpression
names: [ce.name]
}

replaceNode ce, wrapped, parent

function processProgram(root: BlockStatement): void
state := getState()
config := getConfig()
Expand Down Expand Up @@ -1509,16 +1581,17 @@ function processProgram(root: BlockStatement): void
// so their target node can be found in the block without being inside a return
processFunctions(statements, config)

processCoffeeClasses(statements) if config.coffeeClasses

// Insert prelude
statements.unshift(...state.prelude)

if (config.autoLet) {
if config.autoLet
createConstLetDecs(statements, [], "let")
} else if(config.autoConst) {
else if config.autoConst
createConstLetDecs(statements, [], "const")
} else if (config.autoVar) {
else if config.autoVar
createVarDecs(root, [])
}

// REPL wants all top-level variables hoisted to outermost scope
processRepl root, rootIIFE if config.repl
Expand Down Expand Up @@ -1878,6 +1951,7 @@ export {
append
attachPostfixStatementAsExpression
blockWithPrefix
braceBlock
convertNamedImportsToObject
convertObjectToJSXAttributes
convertWithClause
Expand Down
26 changes: 20 additions & 6 deletions source/parser/types.civet
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export type OtherNode =
| CatchPattern
| CaseBlock
| CaseClause
| CoffeeClassPrivate
| CommentNode
| ComputedPropertyName
| ConditionFragment
Expand Down Expand Up @@ -248,7 +249,7 @@ export type AssignmentExpression
type: "AssignmentExpression"
children: Children
parent?: Parent
names: null
names: string[] | null
lhs: AssignmentExpressionLHS
assigned: ASTNode
expression: ExpressionNode
Expand Down Expand Up @@ -952,8 +953,8 @@ export type FunctionExpression
parent?: Parent
name: string
id: Identifier
async: ASTNode[]
generator: ASTNode[]
async?: ASTNode[]
generator?: ASTNode[]
signature: FunctionSignature
block: BlockStatement
parameters: ParametersNode
Expand Down Expand Up @@ -982,11 +983,12 @@ export type MethodDefinition =
children: Children
parent?: Parent
name: string
async: ASTNode[]
generator: ASTNode[]
async?: ASTNode[]
generator?: ASTNode[]
signature: FunctionSignature
block: BlockStatement
parameters: ParametersNode
autoBind?: boolean // CoffeeScript => bound methods

export type ArrowFunction =
type: "ArrowFunction"
Expand Down Expand Up @@ -1084,7 +1086,7 @@ export type FunctionParameter =
type AccessModifier = ASTNode
type ParameterElementDelimiter = ASTNode

export type TypeParameters = unknown
export type TypeParameters = ASTNode

export type FunctionNode = FunctionExpression | ArrowFunction | MethodDefinition

Expand All @@ -1096,6 +1098,7 @@ export type ClassExpression
id: Identifier
heritage: ASTNode
body: ClassBody
binding: [BindingIdentifier, TypeParameters]

export type ClassBody = BlockStatement & { subtype: "ClassBody" }

Expand All @@ -1106,6 +1109,17 @@ export type FieldDefinition
ts?: boolean?
id: ASTNode
typeSuffix?: TypeSuffix?
abstract?: ASTNode
"readonly"?: ASTNode
initializer?: Initializer

export type CoffeeClassPrivate
type: "CoffeeClassPrivate"
children: Children
parent?: Parent
id: ASTNode
typeSuffix?: TypeSuffix?
initializer: Initializer

export type Literal =
type: "Literal"
Expand Down
Loading

0 comments on commit a1ed02f

Please sign in to comment.