-
Notifications
You must be signed in to change notification settings - Fork 8
Added pop() feature #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1943,6 +1943,11 @@ def resolve_args( | |
| if value_expr not in state.synthesized_types: | ||
| continue | ||
|
|
||
| # Skip type check if arg_type is None (sentinel for "any type") | ||
| # Used by builtins like pop that accept any constant-size type | ||
| if arg_type is None: | ||
| continue | ||
|
|
||
| unconverted_type = state.synthesized_types[value_expr] | ||
| if not self.can_coerce_type(unconverted_type, arg_type): | ||
| return CompileError( | ||
|
|
@@ -2002,6 +2007,9 @@ def visit_AstFuncCall(self, node: AstFuncCall, state: CompileState): | |
| # These will be coerced when the function definition is visited. | ||
| if value_expr not in state.synthesized_types: | ||
| continue | ||
| # Skip coercion if target_type is None (sentinel for "any type") | ||
| if target_type is None: | ||
| continue | ||
| assert self.coerce_expr_type(value_expr, target_type, state) | ||
|
|
||
| state.synthesized_types[node] = func.return_type | ||
|
|
@@ -2529,6 +2537,150 @@ def visit_AstFuncCall(self, node: AstFuncCall, state: CompileState): | |
| ) | ||
| return | ||
|
|
||
| # Special validation for pop after confirming args are const | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is hacky. What we actually want is for the normal semantic machinery of the compiler to handle this builtin for us, without having to special case it. Basically for any feature ever this should always be the case. But here, the existing semantic machinery can't express what we want. So the question is what things can we add to the language as a whole that can enable this builtin to work properly, without ever having a special case for it. We already have the ability to define builtin functions, but the problem here is that we can't express the type of the However, there is one problem with this. We don't actually want to accept any time, we just want to accept any type which is serializable (which has a binary form). Some types, like the INTEGER or FLOAT type, have no binary representation (they only exist at compile time and cannot be written into the output file, instead they are coerced at compile time to a serializable type like I64 or F64). So instead what we actually have to do, I think, is define a Serializable type. It should not be accessible to the user, just an internal compiler thing. Actually, even more specifically, it must be a Serializable type whose size we know at compile time. For instance, you could not push the value of a string telemetry channel out this port. So for now, let's call this a SizedType, meaning A) it has a binary representation and B) it has a statically known size of that binary representation. Anything except non-constant strings should be rejected. So make sure you can still output "asdf" for instance. |
||
| if func.name == "pop": | ||
| port_val = arg_values[0] | ||
| value_arg = resolved_args[1] | ||
|
|
||
| # Extract integer value from port_val | ||
| # Validate port value type - must be integer (reject float, Decimal, bool) | ||
| # Python bool is a subclass of int, so check bool first | ||
| if isinstance(port_val.val, bool): | ||
| state.errors.append( | ||
| CompileError( | ||
| "pop port argument must be an integer or enum constant, not bool", | ||
| resolved_args[0] if is_instance_compat(resolved_args[0], Ast) else node, | ||
| ) | ||
| ) | ||
| return | ||
| elif isinstance(port_val.val, (Decimal, float)): | ||
| state.errors.append( | ||
| CompileError( | ||
| f"pop port argument must be an integer or enum constant, not float/Decimal", | ||
| resolved_args[0] if is_instance_compat(resolved_args[0], Ast) else node, | ||
| ) | ||
| ) | ||
| return | ||
| elif isinstance(port_val.val, int): | ||
| # Integer value - OK (includes abstract INTEGER type from literals) | ||
| port_int = port_val.val | ||
| elif isinstance(port_val.val, str): | ||
| # Enum constant: look up the integer value from the enum dict | ||
| if port_val.type.kind == TypeKind.ENUM and hasattr(port_val.type, 'enum_dict'): | ||
| port_int = port_val.type.enum_dict.get(port_val.val) | ||
| if port_int is None: | ||
| state.errors.append( | ||
| CompileError( | ||
| f"Unknown enum value '{port_val.val}' in {port_val.type.name}", | ||
| resolved_args[0] if is_instance_compat(resolved_args[0], Ast) else node, | ||
| ) | ||
| ) | ||
| return | ||
| else: | ||
| state.errors.append( | ||
| CompileError( | ||
| "pop port argument must be an integer or enum constant", | ||
| resolved_args[0] if is_instance_compat(resolved_args[0], Ast) else node, | ||
| ) | ||
| ) | ||
| return | ||
| else: | ||
| # Reject any other type | ||
| state.errors.append( | ||
| CompileError( | ||
| f"pop port argument must be an integer or enum constant, got {type(port_val.val).__name__}", | ||
| resolved_args[0] if is_instance_compat(resolved_args[0], Ast) else node, | ||
| ) | ||
| ) | ||
| return | ||
|
|
||
| # Get MAX_SERIAL_PORTS from the dictionary enum | ||
| serial_port_enum = state.type_defs.get("Svc.Fpy.SerialPortIndex") | ||
| if serial_port_enum is None: | ||
| state.errors.append( | ||
| CompileError( | ||
| "Svc.Fpy.SerialPortIndex enum not found in dictionary", | ||
| node, | ||
| ) | ||
| ) | ||
| return | ||
|
|
||
| max_ports = serial_port_enum.enum_dict.get("MAX_SERIAL_PORTS") | ||
| if max_ports is None: | ||
| state.errors.append( | ||
| CompileError( | ||
| "MAX_SERIAL_PORTS not found in Svc.Fpy.SerialPortIndex enum", | ||
| node, | ||
| ) | ||
| ) | ||
| return | ||
|
|
||
| if not (0 <= port_int < max_ports): | ||
| state.errors.append( | ||
| CompileError( | ||
| f"pop port index {port_int} out of range [0, {max_ports})", | ||
| resolved_args[0] if is_instance_compat(resolved_args[0], Ast) else node, | ||
| ) | ||
| ) | ||
| return | ||
|
|
||
| # Validate value is constant-sized | ||
| if is_instance_compat(value_arg, FpyValue): | ||
| value_type = value_arg.type | ||
| else: | ||
| value_type = state.synthesized_types.get(value_arg) | ||
| if value_type is None: | ||
| state.errors.append( | ||
| CompileError( | ||
| "Cannot determine type of pop value argument", | ||
| value_arg, | ||
| ) | ||
| ) | ||
| return | ||
|
|
||
| # Reject abstract INTEGER/FLOAT types (bare literals without a concrete type) | ||
| # These have no computable max_size and would crash codegen | ||
| if value_type.kind in (TypeKind.INTEGER, TypeKind.FLOAT): | ||
| state.errors.append( | ||
| CompileError( | ||
| f"pop value must have a concrete type (e.g., U32(42) not 42). " | ||
| f"Bare numeric literals are not allowed.", | ||
| value_arg if is_instance_compat(value_arg, Ast) else node, | ||
| ) | ||
| ) | ||
| return | ||
|
|
||
| # Reject anonymous/void kinds that are not serializable and would | ||
| # crash codegen (anon struct/array literals, ranges, void values). | ||
| # INTERNAL_STRING is intentionally excluded here so that bare | ||
| # string literals fall through to the constant-size gate below and | ||
| # produce the "constant-sized" error message users expect. | ||
| if value_type.kind in ( | ||
| TypeKind.ANON_STRUCT, | ||
| TypeKind.ANON_ARRAY, | ||
| TypeKind.RANGE, | ||
| TypeKind.NOTHING, | ||
| ): | ||
| state.errors.append( | ||
| CompileError( | ||
| "pop value must be a concrete, serializable value " | ||
| "(a variable, a type constructor like Ref.SignalPair(...), " | ||
| "or a cast); anonymous literals, ranges, and void " | ||
| "expressions are not allowed.", | ||
| value_arg if is_instance_compat(value_arg, Ast) else node, | ||
| ) | ||
| ) | ||
| return | ||
|
|
||
| if not is_type_constant_size(value_type): | ||
| state.errors.append( | ||
| CompileError( | ||
| f"pop value must be constant-sized (no strings), found {value_type.display_name}", | ||
| value_arg if is_instance_compat(value_arg, Ast) else node, | ||
| ) | ||
| ) | ||
| return | ||
|
|
||
| if unknown_value: | ||
| # we will have to calculate this at runtime | ||
| state.const_expr_values[node] = None | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i don't think
popis a clear name for what this does. Remember that the user of Fpy has no idea that there is a stack, that is an implementation detail. Andpopis a stack term.I would suggest calling this
write_to_portoroutput_portor something.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just taking in from what nasa#5327 had, which was pop(). I also considered serial_pop(), but not sure if we wanted to go that route
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is okay for the bytecode to reference the stack, but the high level language shouldn't really know that it exists. In LLVM it won't exist anyways, so pop just won't make any sense. Plus, when I hear
pop, I think: what are we popping, and what are we going to do with it? I thinkwrite_to_portoroutput_on_portis much more clear.