Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
})
57 changes: 57 additions & 0 deletions src/cse-machine/closure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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,
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