Skip to content

Commit 6c46a92

Browse files
Add pre-assignment type declaration, LAMBDA
1 parent 0fd0957 commit 6c46a92

File tree

5 files changed

+151
-8
lines changed

5 files changed

+151
-8
lines changed

SPECIFICATION.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@
194194

195195
Functions are defined using the `FUNC` keyword with explicit parameter and return types. The canonical positional-only form is `FUNC name(T1: arg1, T2: arg2, ..., TN: argN):R{ block }`, where each `Tk` and `R` is `INT`, `FLT`, `STR`, `TNS`, or `FUNC`. Parameters may also declare a call-time default value using `Tk: arg = expr`. A parameter without a default is positional; a parameter with a default is keyword-capable. Positional parameters must appear before any parameters with defaults. Defining a function binds `name` to a callable body with the specified typed formal parameters. Function names must not conflict with the names of built-in operators or functions.
196196

197+
In addition to named functions, the language provides an anonymous function literal form `LAMBDA` which constructs a `FUNC` value without binding it to a function name in the global function table. The syntax is `LAMBDA(T1: arg1, T2: arg2, ..., TN: argN):R{ block }`. Parameter typing, default-value rules, call semantics, and return-type rules are the same as for `FUNC`. Evaluating a `LAMBDA` expression captures (closes over) the current lexical environment, producing a first-class `FUNC` value that can be assigned to variables, stored in tensors, passed as an argument, or returned.
198+
197199
A user-defined function is called with the same syntax as a built-in: `callee(expr1, expr2, ..., exprN)`. The callee may be any expression that evaluates to `FUNC`, including identifiers, tensor elements, or intermediate expressions. Calls may supply zero or more positional arguments (left-to-right) followed by zero or more keyword arguments of the form `param=expr`. Keyword arguments can only appear after all positional arguments. At the call site, every positional argument is bound to the next positional parameter; keyword arguments must match the name of a parameter that declared a default value. Duplicate keyword names, supplying too many positional arguments, or providing a keyword for an unknown parameter are runtime errors. If a keyword-capable parameter is omitted from the call, its default expression is evaluated at call time in the function's lexical environment after earlier parameters have been bound. The evaluated default must match the parameter's declared type. Built-in functions do not accept keyword arguments except that `READFILE` and `WRITEFILE` allow a single optional `coding=` keyword; attempting to pass any other keyword raises a runtime error. Arguments are evaluated left-to-right. The function body executes in a new environment (activation record) that closes over the defining environment. If a `RETURN(v)` statement is executed, the function terminates immediately and yields `v`; the returned value must match the declared return type. If control reaches the end of the body without `RETURN`, the function returns a default value of the declared return type (0 for `INT`, 0.0 for `FLT`, "" for `STR`). Functions whose return type is
198200
`TNS` or `FUNC` must execute an explicit `RETURN` of the declared type; reaching the end of the body without returning is a runtime error for `TNS`- or `FUNC`-returning functions.
199201
@@ -204,7 +206,7 @@
204206
205207
## 6. Variables and Memory Model
206208
207-
A variable is created only when it is first assigned with an explicit type annotation of the form `TYPE : name = expression`, where TYPE is `INT`, `FLT`, `STR`, `TNS`, or `FUNC`. For example: `INT : counter = 0` or `FUNC : handler = DISPATCH`. Subsequent assignments to the same name must match the declared type and may omit the type annotation (`counter = ADD(counter,1)`). Assigning to an undeclared name without a type annotation is a runtime error. A variable exists until `DEL(name)` is executed. Referencing a variable that has never been declared, or that has been deleted, is a runtime error.
209+
A symbol's type may be declared without assigning a value using the form `TYPE : name` (for example `INT: x`). Such a declaration records the name's type but does not create a runtime binding: reading `name` before it has been assigned still yields an undefined-identifier error. A variable is created when it is first assigned with an explicit type annotation `TYPE : name = expression`, or when a value is assigned to a previously-declared name. Subsequent assignments to the same name must match the declared type and may omit the type annotation (for example `x = 1` after `INT: x`). Assigning to an undeclared name without either an inline type annotation or a prior declaration is a runtime error. A variable exists until `DEL(name)` is executed. Referencing a variable that has never been assigned (even if it was previously declared) or that has been deleted is a runtime error.
208210
209211
The language assumes at least a global typed environment mapping identifiers to (type, value) pairs. Function calls create new environments for parameters and local variables, as described in Section 6.2; the precise details of name resolution depend on the chosen scoping rules.
210212

asm-lang.exe

2.73 KB
Binary file not shown.

interpreter.py

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from extensions import ASMExtensionError, HookRegistry, RuntimeServices, StepContext, TypeContext, TypeRegistry, TypeSpec, build_default_services
1919
from parser import (
2020
Assignment,
21+
Declaration,
2122
Block,
2223
BreakStatement,
2324
CallArgument,
@@ -27,6 +28,7 @@
2728
ForStatement,
2829
ParForStatement,
2930
FuncDef,
31+
LambdaExpression,
3032
AsyncStatement,
3133
GotoStatement,
3234
GotopointStatement,
@@ -152,6 +154,8 @@ def __init__(self, target: Value) -> None:
152154
class Environment:
153155
parent: Optional["Environment"] = None
154156
values: Dict[str, Value] = field(default_factory=dict)
157+
# declared but unassigned symbol types
158+
declared: Dict[str, str] = field(default_factory=dict)
155159
frozen: set = field(default_factory=set)
156160
permafrozen: set = field(default_factory=set)
157161

@@ -213,16 +217,56 @@ def set(self, name: str, value: Value, declared_type: Optional[str] = None) -> N
213217
found_env.values[name] = value
214218
return
215219

220+
# No existing value binding found. Look for a prior type declaration
221+
# in this environment chain.
222+
decl_env: Optional[Environment] = None
223+
decl_type: Optional[str] = None
224+
env2: Optional[Environment] = self
225+
while env2 is not None:
226+
if name in env2.declared:
227+
decl_env = env2
228+
decl_type = env2.declared[name]
229+
break
230+
env2 = env2.parent
231+
216232
if declared_type is None:
217-
raise ASMRuntimeError(
218-
f"Identifier '{name}' must be declared with a type before assignment",
219-
rewrite_rule="ASSIGN",
220-
)
233+
# Assignment without inline declaration: require a prior declaration
234+
if decl_env is None:
235+
raise ASMRuntimeError(
236+
f"Identifier '{name}' must be declared with a type before assignment",
237+
rewrite_rule="ASSIGN",
238+
)
239+
if decl_type != value.type:
240+
raise ASMRuntimeError(
241+
f"Assigned value type {value.type} does not match declaration {decl_type}",
242+
rewrite_rule="ASSIGN",
243+
)
244+
decl_env.values[name] = value
245+
return
246+
247+
# Assignment with inline declaration: if there is an existing declaration
248+
# ensure it matches; otherwise record declaration in current env.
249+
if decl_env is not None:
250+
if decl_type != declared_type:
251+
raise ASMRuntimeError(
252+
f"Type mismatch for '{name}': previously declared as {decl_type}",
253+
rewrite_rule="ASSIGN",
254+
)
255+
if declared_type != value.type:
256+
raise ASMRuntimeError(
257+
f"Assigned value type {value.type} does not match declaration {declared_type}",
258+
rewrite_rule="ASSIGN",
259+
)
260+
decl_env.values[name] = value
261+
return
262+
263+
# No prior declaration: record it in this environment and create the value
221264
if declared_type != value.type:
222265
raise ASMRuntimeError(
223266
f"Assigned value type {value.type} does not match declaration {declared_type}",
224267
rewrite_rule="ASSIGN",
225268
)
269+
self.declared[name] = declared_type
226270
self.values[name] = value
227271

228272
def get(self, name: str) -> Value:
@@ -3486,6 +3530,9 @@ def _func_to_str(ctx: TypeContext, v: Value) -> str:
34863530
self._functions_version: int = 0
34873531
self._call_resolution_cache: Dict[Tuple[str, str, int], Optional[str]] = {}
34883532

3533+
# Monotonic counter for anonymous lambda names (for logs/tracebacks only).
3534+
self._lambda_counter: int = 0
3535+
34893536
def _mark_functions_changed(self) -> None:
34903537
self._functions_version += 1
34913538
self._call_resolution_cache.clear()
@@ -3715,6 +3762,28 @@ def _execute_block(self, statements: List[Statement], env: Environment) -> None:
37153762
def _execute_statement(self, statement: Statement, env: Environment) -> None:
37163763
self._log_step(rule=statement.__class__.__name__, location=statement.location)
37173764
statement_type = type(statement)
3765+
if statement_type is Declaration:
3766+
# Record a type declaration without creating a value. Do not
3767+
# introduce a binding; reads will still raise until the name is
3768+
# assigned. If a value already exists in the same environment,
3769+
# ensure the types match.
3770+
if statement.name in self.functions:
3771+
raise ASMRuntimeError(
3772+
f"Identifier '{statement.name}' already bound as function",
3773+
location=statement.location,
3774+
rewrite_rule="ASSIGN",
3775+
)
3776+
env_found = env._find_env(statement.name)
3777+
if env_found is not None:
3778+
existing = env_found.values.get(statement.name)
3779+
if existing is not None and existing.type != statement.declared_type:
3780+
raise ASMRuntimeError(
3781+
f"Type mismatch for '{statement.name}': previously declared as {existing.type}",
3782+
location=statement.location,
3783+
rewrite_rule="ASSIGN",
3784+
)
3785+
env.declared[statement.name] = statement.declared_type
3786+
return
37183787
if statement_type is Assignment:
37193788
if statement.target in self.functions:
37203789
raise ASMRuntimeError(
@@ -4496,6 +4565,27 @@ def _evaluate_expression(self, expression: Expression, env: Environment) -> Valu
44964565
location=expression.location,
44974566
rewrite_rule="IDENT",
44984567
)
4568+
if expression_type is LambdaExpression:
4569+
# Create an anonymous function value that closes over the current environment.
4570+
self._lambda_counter += 1
4571+
name = f"<lambda_{self._lambda_counter}>"
4572+
fn = Function(
4573+
name=name,
4574+
params=expression.params,
4575+
return_type=expression.return_type,
4576+
body=expression.body,
4577+
closure=env,
4578+
)
4579+
self._log_step(
4580+
rule="LAMBDA",
4581+
location=expression.location,
4582+
extra={
4583+
"name": name,
4584+
"params": [p.name for p in expression.params],
4585+
"return_type": expression.return_type,
4586+
},
4587+
)
4588+
return Value(TYPE_FUNC, fn)
44994589
if expression_type is CallExpression:
45004590
callee_expr = expression.callee
45014591
callee_ident = callee_expr.name if type(callee_expr) is Identifier else None

lexer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class Token:
2727
"FOR",
2828
"PARFOR",
2929
"FUNC",
30+
"LAMBDA",
3031
"ASYNC",
3132
"RETURN",
3233
"POP",

parser.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,16 @@ class Block(Node):
3737
@dataclass(slots=True)
3838
class Assignment(Statement):
3939
target: str
40-
expression: "Expression"
4140
declared_type: Optional[str]
4241

42+
expression: "Expression"
43+
44+
45+
@dataclass(slots=True)
46+
class Declaration(Statement):
47+
name: str
48+
declared_type: str
49+
4350

4451
@dataclass(slots=True)
4552
class ExpressionStatement(Statement):
@@ -134,6 +141,13 @@ class Expression(Node):
134141
pass
135142

136143

144+
@dataclass(slots=True)
145+
class LambdaExpression(Expression):
146+
params: List["Param"]
147+
return_type: str
148+
body: Block
149+
150+
137151
@dataclass(slots=True)
138152
class TensorLiteral(Expression):
139153
items: List["Expression"]
@@ -285,10 +299,17 @@ def _parse_assignment(self, declared_type: Optional[str]) -> Assignment:
285299
location: SourceLocation = self._location_from_token(ident)
286300
return Assignment(location=location, target=ident.value, expression=expr, declared_type=declared_type)
287301

288-
def _parse_typed_assignment(self) -> Assignment:
302+
def _parse_typed_assignment(self) -> Statement:
289303
type_token = self._consume_type_token()
290304
self._consume("COLON")
291-
return self._parse_assignment(type_token.value)
305+
# Allow a bare type declaration (e.g. `INT: x`) which records the
306+
# symbol's declared type but does not perform an assignment. If an
307+
# equals follows, parse as a normal typed assignment.
308+
if self._peek().type == "IDENT" and self._peek_next().type == "EQUALS":
309+
return self._parse_assignment(type_token.value)
310+
ident = self._consume("IDENT")
311+
location: SourceLocation = self._location_from_token(type_token)
312+
return Declaration(location=location, name=ident.value, declared_type=type_token.value)
292313

293314
def _parse_index_assignment(self) -> TensorSetStatement:
294315
target = self._parse_index_expression()
@@ -324,6 +345,33 @@ def _parse_func(self) -> FuncDef:
324345
location: SourceLocation = self._location_from_token(keyword)
325346
return FuncDef(location=location, name=name_token.value, params=params, return_type=return_type.value, body=block)
326347

348+
def _parse_lambda(self) -> LambdaExpression:
349+
keyword = self._consume("LAMBDA")
350+
self._consume("LPAREN")
351+
params: List[Param] = []
352+
seen_default = False
353+
if self._peek().type != "RPAREN":
354+
while True:
355+
type_token = self._consume_type_token()
356+
self._consume("COLON")
357+
name_tok = self._consume("IDENT")
358+
default_expr: Optional[Expression] = None
359+
if self._match("EQUALS"):
360+
seen_default = True
361+
default_expr = self._parse_expression()
362+
elif seen_default:
363+
raise ASMParseError(
364+
f"Positional parameter cannot follow parameter with default at line {name_tok.line}")
365+
params.append(Param(type=type_token.value, name=name_tok.value, default=default_expr))
366+
if not self._match("COMMA"):
367+
break
368+
self._consume("RPAREN")
369+
self._consume("COLON")
370+
return_type = self._consume_type_token()
371+
block: Block = self._parse_block()
372+
location: SourceLocation = self._location_from_token(keyword)
373+
return LambdaExpression(location=location, params=params, return_type=return_type.value, body=block)
374+
327375
def _parse_if(self) -> IfStatement:
328376
keyword = self._consume("IF")
329377
condition: Expression = self._parse_parenthesized_expression()
@@ -452,6 +500,8 @@ def _parse_primary(self) -> Expression:
452500
return self._parse_tensor_literal()
453501
if token.type == "LANGLE":
454502
return self._parse_map_literal()
503+
if token.type == "LAMBDA":
504+
return self._parse_lambda()
455505
if token.type == "IDENT":
456506
ident: Token = self._consume("IDENT")
457507
location: SourceLocation = self._location_from_token(ident)

0 commit comments

Comments
 (0)