You are tasked with implementing a type system for a toy programming language. This language supports:
int, float, strT, T1, T2, S[int, T1, str], [int, str, [int, T1]][param1, param2, ...] -> returnType[int, [int, T1], T2] -> [str, T2, [float, float]]You need to implement two classes: Node and Function.
The Node class represents a type in the language, which can be either: [Source: darkinterview.com]
Constructor:
def __init__(self, node_type: Union[str, List['Node']])
node_type is a string: represents a primitive or generic typenode_type is a list: represents a tuple containing child nodesKey Methods:
__str__(): Returns the string representation of the node (see Part 1)The Function class represents a function signature with parameters and a return type. [Source: darkinterview.com]
Constructor:
def __init__(self, parameters: List[Node], output_type: Node)
parameters: List of Node objects representing function parameter typesoutput_type: A Node object representing the return typeKey Methods:
__str__(): Returns the string representation of the function (see Part 1)Implement the __str__() method for both Node and Function classes. [Source: darkinterview.com]
int → "int", T1 → "T1"[int, float] → "[int,float]"[int, [str, T1]] → "[int,[str,T1]]"(param1,param2,...) -> returnType[int, T1] and return type [T1, str]
"(int,T1) -> [T1,str]"Note: You should write your own test cases for Part 1 to verify correctness.
Implement a function get_return_type(parameters: List[Node], function: Function) -> Node that:
Your implementation must detect and raise errors for: [Source: darkinterview.com]
int but got strT1 is bound to both int and strFunction Definition:
# [T1, T2, int, T1] -> [T1, T2]
func = Function(
[Node('T1'), Node('T2'), Node('int'), Node('T1')],
Node([Node('T1'), Node('T2')])
)
Actual Parameters:
parameters = [Node('int'), Node('str'), Node('int'), Node('int')]
Expected Return: [Source: darkinterview.com]
Node([Node('int'), Node('str')]) # "[int,str]"
Explanation:
T1 is bound to int (from 1st and 4th parameters)T2 is bound to str (from 2nd parameter)[T1, T2] becomes [int, str]Function Definition: Same as Example 1
Actual Parameters: [Source: darkinterview.com]
parameters = [Node('int'), Node('str'), Node('float'), Node('int')]
Expected: Raise exception
Explanation:
int but receives floatFunction Definition: Same as Example 1 [Source: darkinterview.com]
Actual Parameters:
parameters = [Node('int'), Node('str'), Node('int'), Node('str')]
Expected: Raise exception
Explanation: [Source: darkinterview.com]
T1 to intT1 to strFunction Definition:
# [[ T1, float ], T1] -> [T1, [T1, float]]
func = Function(
[
Node([Node('T1'), Node('float')]),
Node('T1')
],
Node([Node('T1'), Node([Node('T1'), Node('float')])])
)
Actual Parameters:
parameters = [
Node([Node('str'), Node('float')]),
Node('str')
]
Expected Return: [Source: darkinterview.com]
Node([Node('str'), Node([Node('str'), Node('float')])])
# "[str,[str,float]]"
Explanation:
T1 is bound to str from both parametersT1 with strFunction Definition:
# [[T1, float], T2, S] -> [S, T1]
func = Function(
[
Node([Node('T1'), Node('float')]),
Node('T2'),
Node('S')
],
Node([Node('S'), Node('T1')])
)
Actual Parameters: [Source: darkinterview.com]
parameters = [
Node([Node('str'), Node('float')]),
Node([Node('int'), Node('str')]),
Node('int')
]
Expected Return:
Node([Node('int'), Node('str')])
# "[int,str]"
Explanation:
T1 is bound to strT2 is bound to [int, str] (tuple)S is bound to int[S, T1] becomes [int, str]You may find it useful to implement: [Source: darkinterview.com]
is_generic_type(node: Node) -> bool: Check if a node contains any generic typesclone(node: Node) -> Node: Deep copy a nodebind_generics(func_param: Node, actual_param: Node, binding_map: dict):
substitute_generics(node: Node, binding_map: dict) -> Node:
1. Validate parameter count matches
2. Initialize empty binding_map = {}
3. For each (func_param, actual_param) pair:
a. If func_param is a generic type:
- Check if already bound in binding_map
- If bound, verify it matches actual_param
- If not bound, add to binding_map
b. If func_param is concrete:
- Verify it exactly matches actual_param
c. If func_param is a tuple:
- Recursively process each child
4. Substitute all generics in return type using binding_map
5. Return the substituted return type
The interviewer will provide comprehensive test cases for Part 2. Your implementation should pass all provided tests, which will cover:
get_return_type are always concrete (no generic types like T1, T2)Below is a reference implementation demonstrating one approach to solving this problem:
from typing import Union, List
class Node:
def __init__(self, node_type: Union[str, List['Node']]):
self.type_list = ['str', 'float', 'int']
if isinstance(node_type, str):
self.base = node_type
self.children = []
else:
self.base = None
self.children = node_type
def get_content(self) -> Union[str, List['Node']]:
if self.base:
return self.base
return self.children
def is_base_generic_type(self):
return self.base and self.base not in self.type_list
def is_generic_type(self):
if self.is_base_generic_type():
return True
return ([child.is_generic_type() child .children])
():
.base:
Node(.base)
Node([child.clone() child .children])
() -> :
.base:
.base
node_types = []
child .children:
node_types.append((child))
() -> :
(other, Node):
(other) == ()
:
():
.parameters = param
.output_type = output
() -> :
param_str = .join([(param) param .parameters])
output_str = (.output_type)
():
func_param.is_generic_type() func_param.base:
func_param.base binding_map binding_map[func_param.base] != param:
Exception()
func_param.base binding_map:
binding_map[func_param.base] = param
func_param == param:
func_param.base param.base:
(func_param.children) != (param.children):
Exception()
sub_func_node, sub_param_node (func_param.children, param.children):
binding(sub_func_node, sub_param_node, binding_map)
:
Exception()
() -> Node:
node.is_generic_type():
node.clone()
node.children:
cloned = binding_map[node.base].clone()
Node(cloned.get_content())
Node([replace_invocation_arguments(child, binding_map) child node.children])
() -> Node:
(parameters) != (function.parameters):
Exception()
binding_map = {}
func_node, param_node (function.parameters, parameters):
binding(func_node, param_node, binding_map)
function.output_type.is_generic_type():
function.output_type
replace_invocation_arguments(function.output_type, binding_map)
__name__ == :
node1 = Node()
node2 = Node()
node3 = Node()
node4 = Node([node1, node2])
node5 = Node([node4, node3])
(node5)
func = Function([node5, Node()], Node([Node(), Node()]))
(func)
node11 = Node()
node22 = Node()
node33 = Node()
node44 = Node([node11, node22])
node55 = Node([node44, node33])
node = get_return_type([node55, Node([Node(), Node()])], func)
(node)
is_base_generic_type(): Checks if a base type is generic by verifying it's not in the primitive type listis_generic_type(): Recursively checks if a node or any of its children contain genericsbinding(): Recursively matches function parameters with actual parameters, populating the binding map and validating typesreplace_invocation_arguments(): Substitutes all generic types in the return type with their concrete bindings