Skip to content
Merged
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
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@

## What is py-slang?

`py-slang` is a language frontend for the
[js-slang](https://github.com/source-academy/js-slang) repository. It parses
a restricted subset of Python (enough to complete SICP), and outputs an
`estree`-compatible AST. [The grammar](./src/Grammar.gram) is a reduced
version of [Python 3.7's](https://docs.python.org/3.7/reference/grammar.html).
This project does not aim to be a full Python to JS transpiler, but aims
to transpile just a small enough subset of Python.
`py-slang` is a Python implementation developed specifically for the Source Academy online learning environment. Unlike previous versions where Python was treated as a subset within [js-slang](https://github.com/source-academy/js-slang), py-slang now stands as an independent language implementation. It features its own parser, csemachine, and runtime, designed to process a tailored subset of Python for educational purposes.

## Usage
For local testing:
Expand Down
158 changes: 158 additions & 0 deletions src/cse-machine/ast-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// astHelpers.ts
import type * as es from 'estree';
import { StatementSequence } from './types';
import { ControlItem } from './control';

/**
* Create a StatementSequence node.
*/
export const statementSequence = (
body: es.Statement[],
loc?: es.SourceLocation | null
): StatementSequence => ({
type: 'StatementSequence',
body,
loc,
innerComments: undefined,
});

export const isNode = (item: any): item is es.Node => {
return typeof item === 'object' && item !== null && 'type' in item;
};

export const isBlockStatement = (node: es.Node | StatementSequence): node is es.BlockStatement => {
return node.type === 'BlockStatement';
};

export const hasDeclarations = (node: es.BlockStatement): boolean => {
return node.body.some(stmt => stmt.type === 'VariableDeclaration' || stmt.type === 'FunctionDeclaration');
};

export const blockArrowFunction = (
params: es.Identifier[],
body: es.Statement[] | es.BlockStatement | es.Expression,
loc?: es.SourceLocation | null
): es.ArrowFunctionExpression => ({
type: 'ArrowFunctionExpression',
expression: false,
generator: false,
params,
body: Array.isArray(body) ? blockStatement(body) : body,
loc
})

export const blockStatement = (
body: es.Statement[],
loc?: es.SourceLocation | null
): es.BlockStatement => ({
type: 'BlockStatement',
body,
loc
})

export const constantDeclaration = (
name: string,
init: es.Expression,
loc?: es.SourceLocation | null
) => declaration(name, 'declaration', init, loc)

export const declaration = (
name: string,
kind: AllowedDeclarations,
init: es.Expression,
loc?: es.SourceLocation | null
): pyVariableDeclaration => ({
type: 'VariableDeclaration',
declarations: [
{
type: 'VariableDeclarator',
id: identifier(name),
init
}
],
kind: 'declaration',
loc
})

type AllowedDeclarations = 'declaration' | 'const'

export interface pyVariableDeclaration {
type: "VariableDeclaration";
declarations: pyVariableDeclarator[];
kind: "declaration" | "const";
loc?: es.SourceLocation | null | undefined;
range?: [number, number] | undefined;
}

export interface pyVariableDeclarator {
type: "VariableDeclarator";
id: Pattern;
init?: es.Expression | null | undefined;
}

export type Pattern = es.Identifier | es.ObjectPattern | es.ArrayPattern | es.RestElement | es.AssignmentPattern | es.MemberExpression;

export const identifier = (name: string, loc?: es.SourceLocation | null): es.Identifier => ({
type: 'Identifier',
name,
loc
})

export const returnStatement = (
argument: es.Expression,
loc?: es.SourceLocation | null
): es.ReturnStatement => ({
type: 'ReturnStatement',
argument,
loc
})

export const hasReturnStatement = (block: es.BlockStatement | StatementSequence): boolean => {
let hasReturn = false
for (const statement of block.body) {
if (isReturnStatement(statement)) {
hasReturn = true
} else if (isIfStatement(statement)) {
// Parser enforces that if/else have braces (block statement)
hasReturn = hasReturn || hasReturnStatementIf(statement as es.IfStatement)
} else if (isBlockStatement(statement) || isStatementSequence(statement)) {
hasReturn = hasReturn && hasReturnStatement(statement)
}
}
return hasReturn
}

export const isReturnStatement = (node: es.Node): node is es.ReturnStatement => {
return (node as es.ReturnStatement).type == 'ReturnStatement'
}

export const isIfStatement = (node: es.Node): node is es.IfStatement => {
return (node as es.IfStatement).type == 'IfStatement'
}

export const hasReturnStatementIf = (statement: es.IfStatement): boolean => {
let hasReturn = true
// Parser enforces that if/else have braces (block statement)
hasReturn = hasReturn && hasReturnStatement(statement.consequent as es.BlockStatement)
if (statement.alternate) {
if (isIfStatement(statement.alternate)) {
hasReturn = hasReturn && hasReturnStatementIf(statement.alternate as es.IfStatement)
} else if (isBlockStatement(statement.alternate) || isStatementSequence(statement.alternate)) {
hasReturn = hasReturn && hasReturnStatement(statement.alternate)
}
}
return hasReturn
}

export const isStatementSequence = (node: ControlItem): node is StatementSequence => {
return (node as StatementSequence).type == 'StatementSequence'
}

export const literal = (
value: string | number | boolean | null,
loc?: es.SourceLocation | null
): es.Literal => ({
type: 'Literal',
value,
loc
})
58 changes: 58 additions & 0 deletions src/cse-machine/closure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as es from 'estree'
import { Environment } from './environment'
import { Context } from './context'
import { StatementSequence } from './types'
import { blockArrowFunction, blockStatement, hasReturnStatement, identifier, isBlockStatement, returnStatement } from './ast-helper'
import { ControlItem } from './control'

export class Closure {
public originalNode?: es.ArrowFunctionExpression

/** Unique ID defined for closure */
//public readonly id: string

/** Name of the constant declaration that the closure is assigned to */
public declaredName?: string

constructor(
public node: es.ArrowFunctionExpression,
public environment: Environment,
public context: Context,
public predefined: boolean = false
) {
this.originalNode = node
}

static makeFromArrowFunction(
node: es.ArrowFunctionExpression,
environment: Environment,
context: Context,
// TODO: Consider implementing a mechanism that more closely mimics Python’s implicit return (i.e., automatically inserting "return None")
dummyReturn: boolean = false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is part of the CSE machine spec right? I don't think we need a dummyReturn in Python though. CPython just inserts a return None implicitly in the function.

Nothing to improve here, just me grumbling :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's just mimicking CPython's implicit return of None? I'll add a todo for better implementation options.

predefined: boolean = false
): Closure {
const functionBody: es.BlockStatement | StatementSequence =
!isBlockStatement(node.body) && !isStatementSequence(node.body)
? blockStatement([returnStatement(node.body, node.body.loc)], node.body.loc)
: dummyReturn && !hasReturnStatement(node.body)
? blockStatement(
[
...node.body.body,
returnStatement(identifier('undefined', node.body.loc), node.body.loc)
],
node.body.loc
)
: node.body

const closure = new Closure(blockArrowFunction(node.params as es.Identifier[], functionBody, node.loc),
environment, context, predefined)

closure.originalNode = node

return closure
}
}

export const isStatementSequence = (node: ControlItem): node is StatementSequence => {
return (node as StatementSequence).type == 'StatementSequence'
}
Loading