Skip to content

Commit e391c4f

Browse files
committed
Added If, ternary, print (Refs #51)[13]
* added print() for testing on front end * reworked interpreter to be more script runner and not repl like output * changes 1
1 parent ae36bb9 commit e391c4f

File tree

7 files changed

+176
-62
lines changed

7 files changed

+176
-62
lines changed

src/cse-machine/py_context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { StmtNS } from '../ast-types';
1010
export class PyContext {
1111
public control: PyControl;
1212
public stash: Stash;
13+
public output: string = '';
1314
//public environment: Environment;
1415
public errors: CseError[] = [];
1516

src/cse-machine/py_interpreter.ts

Lines changed: 97 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import { PyClosure } from './py_closure';
1111
import { PyContext } from './py_context';
1212
import { PyControl, PyControlItem } from './py_control';
1313
import { createEnvironment, currentEnvironment, pushEnvironment, popEnvironment } from './py_environment';
14-
import { PyNode, Instr, InstrType, UnOpInstr, BinOpInstr, BoolOpInstr, AssmtInstr, AppInstr } from './py_types';
14+
import { PyNode, Instr, InstrType, UnOpInstr, BinOpInstr, BoolOpInstr, AssmtInstr, AppInstr, BranchInstr } from './py_types';
1515
import { Stash, Value, ErrorValue } from './stash';
1616
import { IOptions } from '..';
1717
import * as instrCreator from './py_instrCreator';
18-
import { evaluateUnaryExpression, evaluateBinaryExpression, evaluateBoolExpression } from './py_operators';
18+
import { evaluateUnaryExpression, evaluateBinaryExpression, evaluateBoolExpression, isFalsy } from './py_operators';
1919
import { TokenType } from '../tokens';
2020
import { Token } from '../tokenizer';
2121
import { Result, Finished, CSEBreak, Representation} from '../types';
@@ -66,9 +66,9 @@ export function PyCSEResultPromise(context: PyContext, value: Value): Promise<Re
6666
*/
6767
export function PyEvaluate(code: string, program: StmtNS.Stmt, context: PyContext, options: IOptions): Value {
6868
try {
69-
context.control = new PyControl(program);
7069
context.runtime.isRunning = true;
71-
70+
context.control = new PyControl(program);
71+
7272
const result = pyRunCSEMachine(
7373
code,
7474
context,
@@ -78,7 +78,7 @@ export function PyEvaluate(code: string, program: StmtNS.Stmt, context: PyContex
7878
options.stepLimit,
7979
options.isPrelude || false,
8080
);
81-
return result;
81+
return context.output ? { type: "string", value: context.output} : { type: 'undefined' };
8282
} catch(error: any) {
8383
return { type: 'error', message: error.message};
8484
} finally {
@@ -98,7 +98,7 @@ export function PyEvaluate(code: string, program: StmtNS.Stmt, context: PyContex
9898
* @param isPrelude Whether the program is the prelude.
9999
* @returns The top value of the stash after execution.
100100
*/
101-
function pyRunCSEMachine(
101+
export function pyRunCSEMachine(
102102
code: string,
103103
context: PyContext,
104104
control: PyControl,
@@ -397,6 +397,37 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = {
397397
}
398398
},
399399

400+
'If': (code, command, context, control, stash, isPrelude) => {
401+
const ifNode = command as StmtNS.If;
402+
403+
// create branch instruction, wrap statement arrays in 'StatementSequence' objects
404+
const branch = instrCreator.branchInstr(
405+
{ type: 'StatementSequence', body: ifNode.body },
406+
ifNode.elseBlock
407+
? (Array.isArray(ifNode.elseBlock)
408+
// 'else' block
409+
? {type: 'StatementSequence', body: ifNode.elseBlock }
410+
// 'elif' block
411+
: ifNode.elseBlock)
412+
// 'else' block dont exist
413+
: null,
414+
ifNode
415+
);
416+
control.push(branch);
417+
control.push(ifNode.condition);
418+
},
419+
420+
'Ternary': (code, command, context, control, stash, isPrelude) => {
421+
const ternaryNode = command as ExprNS.Ternary;
422+
const branch = instrCreator.branchInstr(
423+
ternaryNode.consequent,
424+
ternaryNode.alternative,
425+
ternaryNode
426+
);
427+
control.push(branch);
428+
control.push(ternaryNode.predicate);
429+
},
430+
400431
/**
401432
* Instruction Handlers
402433
*/
@@ -471,31 +502,39 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = {
471502
args.unshift(stash.pop());
472503
}
473504

474-
// pop closure from stash
475-
const closure = stash.pop() as PyClosure;
476-
477-
// push reset and implicit return for cleanup at end of function
478-
control.push(instrCreator.resetInstr(instr.srcNode));
505+
// pop callable from stash
506+
const callable = stash.pop();
479507

480-
// Only push endOfFunctionBodyInstr for functionDef
481-
if (closure.node.constructor.name === 'FunctionDef') {
482-
control.push(instrCreator.endOfFunctionBodyInstr(instr.srcNode));
483-
}
508+
if (callable instanceof PyClosure) {
509+
// User-defined function
510+
const closure = callable as PyClosure;
511+
// push reset and implicit return for cleanup at end of function
512+
control.push(instrCreator.resetInstr(instr.srcNode));
484513

485-
// create new function environment
486-
const newEnv = createEnvironment(context, closure, args, instr.srcNode as ExprNS.Call);
487-
pushEnvironment(context, newEnv);
514+
// Only push endOfFunctionBodyInstr for functionDef
515+
if (closure.node.constructor.name === 'FunctionDef') {
516+
control.push(instrCreator.endOfFunctionBodyInstr(instr.srcNode));
517+
}
488518

489-
// push function body onto control stack
490-
const closureNode = closure.node;
491-
if (closureNode.constructor.name === 'FunctionDef') {
492-
// 'def' has a body of statements (an array)
493-
const bodyStmts = (closureNode as StmtNS.FunctionDef).body.slice().reverse();
494-
control.push(...bodyStmts);
519+
// create new function environment
520+
const newEnv = createEnvironment(context, closure, args, instr.srcNode as ExprNS.Call);
521+
pushEnvironment(context, newEnv);
522+
523+
// push function body onto control stack
524+
const closureNode = closure.node;
525+
if (closureNode.constructor.name === 'FunctionDef') {
526+
// 'def' has a body of statements (an array)
527+
const bodyStmts = (closureNode as StmtNS.FunctionDef).body.slice().reverse();
528+
control.push(...bodyStmts);
529+
} else {
530+
// 'lambda' has a body with a single expression
531+
const bodyExpr = (closureNode as ExprNS.Lambda).body;
532+
control.push(bodyExpr);
533+
}
495534
} else {
496-
// 'lambda' has a body with a single expression
497-
const bodyExpr = (closureNode as ExprNS.Lambda).body;
498-
control.push(bodyExpr);
535+
// Built-in function from stdlib / constants
536+
const result = (callable as any)(context, ...args);
537+
stash.push(result);
499538
}
500539
},
501540

@@ -508,4 +547,35 @@ const pyCmdEvaluators: { [type: string]: CmdEvaluator } = {
508547
stash.push({ type: 'undefined' });
509548
},
510549

550+
[InstrType.BRANCH]: (code, command, context, control, stash, isPrelude) => {
551+
const instr = command as BranchInstr;
552+
const condition = stash.pop();
553+
554+
if (!isFalsy(condition)) {
555+
// Condition is truthy, execute the consequent
556+
const consequent = instr.consequent;
557+
if (consequent && 'type' in consequent && consequent.type === 'StatementSequence') {
558+
control.push(...(consequent as any).body.slice().reverse());
559+
} else if (consequent) {
560+
// consequent of ternary or single statement
561+
control.push(consequent);
562+
}
563+
} else if (instr.alternate) {
564+
// Condition is falsy, execute the alternate
565+
const alternate = instr.alternate;
566+
if (alternate && 'type' in alternate && alternate.type === 'StatementSequence') {
567+
// 'else' block
568+
control.push(...(alternate as any).body.slice().reverse());
569+
} else if (alternate) {
570+
// 'elif' or ternary alternative
571+
control.push(alternate);
572+
}
573+
}
574+
// If condition is falsy and there's no alternate, do nothing
575+
},
576+
577+
[InstrType.POP]: (code, command, context, control, stash, isPrelude) => {
578+
stash.pop();
579+
},
580+
511581
};

src/cse-machine/py_operators.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Value } from "./stash";
22
import { PyContext } from "./py_context";
33
import { PyComplexNumber} from "../types";
4-
import { UnsupportedOperandTypeError, ZeroDivisionError, TypeConcatenateError } from "../errors/py_errors";
4+
import { UnsupportedOperandTypeError, ZeroDivisionError } from "../errors/py_errors";
55
import { ExprNS } from "../ast-types";
66
import { TokenType } from "../tokens";
77
import { pyHandleRuntimeError, operatorTranslator, pythonMod, typeTranslator } from "./py_utils";
@@ -33,7 +33,7 @@ export type BinaryOperator =
3333
| "instanceof";
3434

3535
// Helper function for truthiness based on Python rules
36-
function isFalsy(value: Value): boolean {
36+
export function isFalsy(value: Value): boolean {
3737
switch (value.type) {
3838
case 'bigint':
3939
return value.value === 0n;
@@ -179,11 +179,12 @@ export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, con
179179
if (left.type === 'string' && right.type === 'string') {
180180
return { type: 'string', value: left.value + right.value };
181181
} else {
182-
const wrongType = left.type === 'string' ? right.type : left.type;
183-
pyHandleRuntimeError(context, new TypeConcatenateError(
182+
pyHandleRuntimeError(context, new UnsupportedOperandTypeError(
184183
code,
185184
command,
186-
typeTranslator(wrongType)
185+
left.type,
186+
right.type,
187+
operatorTranslator(operator)
187188
));
188189
}
189190
}
@@ -257,7 +258,7 @@ export function evaluateBinaryExpression(code: string, command: ExprNS.Expr, con
257258
}
258259
return { type: 'number', value: pythonMod(l, r) };
259260
case TokenType.DOUBLESTAR:
260-
if (r === 0) {
261+
if (l === 0 && r < 0) {
261262
pyHandleRuntimeError(context, new ZeroDivisionError(code, command, context));
262263
}
263264
return { type: 'number', value: l ** r };

src/cse-machine/py_types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ export interface BranchInstr extends BaseInstr {
109109
alternate: PyNode | null | undefined
110110
}
111111

112+
export interface PopInstr extends BaseInstr {
113+
instrType: InstrType.POP;
114+
}
115+
112116
export interface EnvInstr extends BaseInstr {
113117
env: Environment
114118
}
@@ -126,6 +130,7 @@ export type Instr =
126130
| EndOfFunctionBodyInstr
127131
| ResetInstr
128132
| BranchInstr
133+
| PopInstr
129134
| EnvInstr
130135
| ArrLitInstr
131136
| UnOpInstr

src/cse-machine/py_utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { PyNode } from "./py_types";
44
import { TokenType } from "../tokens";
55
import { PyRuntimeSourceError } from "../errors/py_runtimeSourceError";
66
import { currentEnvironment, PyEnvironment } from "./py_environment";
7+
import { builtIns } from "../py_stdlib";
78

89

910
export function pyHandleRuntimeError (context: PyContext, error: PyRuntimeSourceError) {
@@ -111,6 +112,9 @@ export function pyGetVariable(context: PyContext, name: string, node: PyNode): V
111112
environment = environment.tail;
112113
}
113114
}
115+
if (builtIns.has(name)) {
116+
return builtIns.get(name)!;
117+
}
114118
// For now, we throw an error. We can change this to return undefined if needed.
115119
// handleRuntimeError(context, new TypeError(`name '${name} is not defined`, node as any, context as any, '', ''));
116120
return { type: 'error', message: `NameError: name '${name}' is not defined` };

src/errors/py_errors.ts

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,26 +34,24 @@ export function createErrorIndicator(snippet: string, errorPos: number): string
3434
return indicator;
3535
}
3636

37-
export class TypeConcatenateError extends PyRuntimeSourceError {
38-
constructor(source: string, node: ExprNS.Expr, wrongType: string) {
39-
super(node);
40-
this.type = ErrorType.TYPE;
41-
42-
const { line, fullLine } = getFullLine(source, node.startToken.indexInSource);
43-
44-
const snippet = source.substring(node.startToken.indexInSource, node.endToken.indexInSource + node.endToken.lexeme.length);
45-
const offset = fullLine.indexOf(snippet);
46-
const adjustedOffset = offset >= 0 ? offset : 0;
47-
48-
const errorPos = (node as any).operator.indexInSource - node.startToken.indexInSource;
49-
const indicator = createErrorIndicator(snippet, errorPos);
50-
const name = "TypeError";
51-
let hint = 'TypeError: can only concatenate str (not "' + wrongType + '") to str.';
52-
const suggestion = "You are trying to concatenate a string with an " + wrongType + ". To fix this, convert the " + wrongType + " to a string using str(), or ensure both operands are of the same type.";
53-
const msg = `${name} at line ${line}\n\n ${fullLine}\n ${' '.repeat(adjustedOffset)}${indicator}\n${hint}\n${suggestion}`;
54-
this.message = msg;
55-
}
56-
}
37+
// export class TypeConcatenateError extends PyRuntimeSourceError {
38+
// constructor(source: string, node: ExprNS.Expr, wrongType: string) {
39+
// super(node);
40+
// this.type = ErrorType.TYPE;
41+
42+
// let index = (node as any).symbol?.loc?.start?.index;
43+
// const { line, fullLine } = getFullLine(source, index);
44+
// const snippet = (node as any).symbol?.loc?.source ?? '<unknown source>';
45+
46+
// let hint = 'TypeError: can only concatenate str (not "' + wrongType + '") to str.';
47+
// const offset = fullLine.indexOf(snippet);
48+
// const indicator = createErrorIndicator(snippet, '+');
49+
// const name = "TypeError";
50+
// const suggestion = "You are trying to concatenate a string with an " + wrongType + ". To fix this, convert the " + wrongType + " to a string using str(), or ensure both operands are of the same type.";
51+
// const msg = name + " at line " + line + "\n\n " + fullLine + "\n " + " ".repeat(offset) + indicator + "\n" + hint + "\n" + suggestion;
52+
// this.message = msg;
53+
// }
54+
// }
5755

5856
export class UnsupportedOperandTypeError extends PyRuntimeSourceError {
5957
constructor(source: string, node: ExprNS.Expr, wrongType1: string, wrongType2: string, operand: string) {

src/py_stdlib.ts

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { Closure } from "./cse-machine/closure";
1+
import { PyContext } from './cse-machine/py_context';
2+
import { PyClosure } from './cse-machine/py_closure';
23
import { Value } from "./cse-machine/stash";
3-
4+
import { pyHandleRuntimeError } from "./cse-machine/py_utils";
5+
import { UnsupportedOperandTypeError } from "./errors/py_errors";
46

57
export function toPythonFloat(num: number): string {
68
if (Object.is(num, -0)) {
@@ -32,6 +34,9 @@ export function toPythonFloat(num: number): string {
3234

3335
export function toPythonString(obj: Value): string {
3436
let ret: any;
37+
if (!obj) {
38+
return 'None';
39+
}
3540
if ((obj as Value).type === 'bigint' || (obj as Value).type === 'complex') {
3641
ret = (obj as Value).value.toString();
3742
} else if ((obj as Value).type === 'number') {
@@ -44,16 +49,46 @@ export function toPythonString(obj: Value): string {
4449
}
4550
} else if ((obj as Value).type === 'error') {
4651
return (obj as Value).message;
47-
} else if ((obj as unknown as Closure).node) {
48-
for (let name in (obj as unknown as Closure).environment!.head) {
49-
if ((obj as unknown as Closure).environment!.head[name] === obj) {
50-
return '<function ' + name + '>';
51-
}
52+
} else if (obj instanceof PyClosure) {
53+
if (obj.node) {
54+
const funcName = (obj.node as any).name?.lexeme || '(anonymous)';
55+
return `<function ${funcName}>`;
5256
}
53-
} else if ((obj as Value) === undefined || (obj as Value).value === undefined) {
57+
} else if ((obj as Value).value === undefined) {
5458
ret = 'None';
5559
} else {
5660
ret = (obj as Value).value.toString();
5761
}
5862
return ret;
59-
}
63+
}
64+
65+
export class BuiltInFunctions {
66+
static print(context: PyContext, ...args: Value[]): Value {
67+
const output = args.map(arg => toPythonString(arg)).join(' ');
68+
context.output += output + '\n';
69+
return { type: 'undefined' };
70+
}
71+
72+
static _int(context: PyContext, ...args: Value[]): Value {
73+
if (args.length === 0) {
74+
return { type: 'bigint', value: BigInt(0) };
75+
}
76+
77+
const arg = args[0];
78+
if (arg.type === 'number') {
79+
const truncated = Math.trunc(arg.value);
80+
return { type: 'bigint', value: BigInt(truncated) };
81+
}
82+
if (arg.type === 'bigint') {
83+
return { type: 'bigint', value: arg.value };
84+
}
85+
86+
// TODO: Use proper TypeError class once node is passed to built-ins
87+
return { type: 'error', message: `TypeError: int() argument must be a string, a bytes-like object or a real number, not '${arg.type}'` };
88+
}
89+
}
90+
91+
// Load only the functions we have implemented
92+
export const builtIns = new Map<string, (...args: any[]) => any>();
93+
builtIns.set('print', BuiltInFunctions.print);
94+
builtIns.set('_int', BuiltInFunctions._int);

0 commit comments

Comments
 (0)