Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions conformance/core.json
Original file line number Diff line number Diff line change
Expand Up @@ -294,5 +294,10 @@
"name": "typed Remember field statements",
"source": "Create a class called Counter:\n When creating a Counter with initial:\n Remember initial as this.value.\n End constructor.\n To increment:\n Remember this.value plus 1 as this.value.\n End function.\nEnd class.\nLet counter be a new Counter with 4.\nTell counter to increment.\nSay counter.value.",
"output": "5\n"
},
{
"name": "typed Call and Run statements",
"source": "To combine with left and right:\n Say left plus right.\nEnd function.\nCall combine with 2 and 3.\nRun combine with 4 and 1.",
"output": "5\n5\n"
}
]
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Tell source
-> runtime environment and built-ins
```

Common assignment nodes now also include indexed collection assignment (`Set values[index] to value.`, `Set the item at position index in values to value.`, and `Set the key key in mapping to value.`), with the same typed node executed by both source interpreters and lowered to a verified `SET_INDEX` bytecode operation for list and dictionary writes. Import statements for local Tell modules, Python modules, JavaScript modules, and built-in libraries are also represented as typed nodes; the compiler reuses its existing embedded-module and interop instructions for those nodes. Constructor and method `Remember expression as this.field.` assignments are also typed; the source runtimes update the active instance directly, while bytecode emits the existing `LOAD this`/`SET_FIELD` sequence.
Common assignment nodes now also include indexed collection assignment (`Set values[index] to value.`, `Set the item at position index in values to value.`, and `Set the key key in mapping to value.`), with the same typed node executed by both source interpreters and lowered to a verified `SET_INDEX` bytecode operation for list and dictionary writes. Import statements for local Tell modules, Python modules, JavaScript modules, and built-in libraries are also represented as typed nodes; the compiler reuses its existing embedded-module and interop instructions for those nodes. Constructor and method `Remember expression as this.field.` assignments are also typed; the source runtimes update the active instance directly, while bytecode emits the existing `LOAD this`/`SET_FIELD` sequence. `Call function with arguments.` and `Run function with arguments.` are typed call statements that evaluate a call AST and discard its result, matching the existing statement semantics in source and bytecode execution.

The source interpreter path is shown above. The native CLI also has an early compiler path for a limited statement subset:

Expand Down
5 changes: 4 additions & 1 deletion docs/GRAMMAR_V1.ebnf
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ The logical words and, or, and not are operators. The word in and phrase is in a
return-statement = ("Give back" | "Return"), [ expression ], "." ;
raise-statement = "Raise an error saying", expression, "." ;
call-statement = "Tell", identifier, "to", identifier,
[ "with", argument-list ], "." ;
[ "with", argument-list ], "."
| ("Call" | "Run"), identifier,
[ "with", call-arguments ], "." ;
call-arguments = expression, { "and", expression } ;
import-statement = "Use the module", string, [ "as", identifier ], "."
| "Use the Python library called", identifier, [ "as", identifier ], "."
| "Use the JavaScript library called", string, [ "as", identifier ], "."
Expand Down
1 change: 1 addition & 0 deletions docs/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ The v0.3.6 implementation is committed to `main` and published at https://github


## 2026-11-12 modernization branch checkpoint
- Continued on `modernization/0.4-call-run-ast`: `Call` and `Run` function statements are now represented as typed call-expression nodes in both interpreters; bytecode lowers them to CALL followed by POP so results are discarded like the statement runtime. Added source/bytecode tests and a shared conformance case.
- Continued on `modernization/0.4-remember-ast`: `Remember expression as this.field.` is parsed and executed as a typed statement in Python and Go, compiled through `SET_FIELD`, and covered by constructor/method source tests, bytecode tests, and the shared cross-runtime corpus.

The following work is on `modernization/0.4-foundation`; it has not been merged to `main` or published as a release.
Expand Down
2 changes: 2 additions & 0 deletions native/internal/tell/bytecode.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,8 @@ func (c *bytecodeCompiler) compileSimpleStatement(statement *simpleStatement) er
c.code[instruction].Name = statement.names[0]
c.code[instruction].Type = statement.annotation
c.code[instruction].Constant = statement.constant
case "call":
c.emit("POP")
case "set":
if len(statement.names) > 1 {
instruction := c.emit("MULTI_ASSIGN")
Expand Down
11 changes: 11 additions & 0 deletions native/internal/tell/bytecode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,17 @@ func TestBytecodePreservesRuntimeTypeAndConstantRules(t *testing.T) {
}
}

func TestBytecodeCompilesCallAndRunStatements(t *testing.T) {
source := `To combine with left and right:
Say left plus right.
End function.
Call combine with 2 and 3.
Run combine with 4 and 1.`
if got, want := runCompiled(t, source), "5\n5\n"; got != want {
t.Fatalf("compiled Call/Run output = %q, want %q", got, want)
}
}

func TestBytecodeCompilesRememberFieldStatements(t *testing.T) {
source := `Create a class called Counter:
When creating a Counter with initial:
Expand Down
2 changes: 2 additions & 0 deletions native/internal/tell/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ func (r *runtime) executeSimpleStatement(statement *simpleStatement, e *env) err
return r.declare(e, statement.names[0], value, statement.annotation, statement.constant)
}
return r.assign(e, statement.names[0], value)
case "call":
return nil
case "set":
if len(statement.names) > 1 {
values, ok := sequenceValues(value)
Expand Down
29 changes: 29 additions & 0 deletions native/internal/tell/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,35 @@ func parseSimpleStatement(source string, line, column int) *simpleStatement {
}
}
}
for _, prefix := range []string{"call ", "run "} {
rest, ok := prefixFold(source, prefix)
if !ok { continue }
name, tail := firstWord(rest)
if !identifier(name) { continue }
arguments := []callArgument{}
valid := true
if strings.TrimSpace(tail) != "" {
argumentText, hasWith := prefixFold(tail, "with ")
if !hasWith || strings.TrimSpace(argumentText) == "" {
valid = false
} else {
for _, argument := range splitAnd(argumentText) {
parsed, parsedOK := parseExpression(argument)
if !parsedOK { valid = false; break }
arguments = append(arguments, callArgument{value:parsed})
}
}
}
if valid {
nameOffset := strings.Index(source, name)
if nameOffset < 0 { nameOffset = len(prefix) }
callee := nameExpression{name:name, line:line, column:column+utf8.RuneCountInString(source[:nameOffset])}
callOffset := strings.Index(strings.ToLower(source), strings.TrimSpace(prefix))
if callOffset < 0 { callOffset = 0 }
call := callExpression{callee:callee, args:arguments, line:line, column:column+utf8.RuneCountInString(source[:callOffset])}
return &simpleStatement{kind:"call", source:source, expression:call, hasExpression:true}
}
}
if rest, ok := prefixFold(source, "remember "); ok {
expressionText, field, separated := splitFoldOnce(rest, " as this.")
if separated && identifier(field) {
Expand Down
23 changes: 23 additions & 0 deletions native/internal/tell/tell_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,29 @@ func TestIndexedAssignmentsUseTypedASTNodesAndExecute(t *testing.T) {
}
}

func TestCallAndRunStatementsUseTypedASTNodesAndExecute(t *testing.T) {
source := "To combine with left and right:\n Say left plus right.\nEnd function.\nCall combine with 2 and 3.\nRun combine with 4 and 1."
nodes, err := parse(source, "call-statements.tell")
if err != nil {
t.Fatal(err)
}
for _, index := range []int{1, 2} {
if nodes[index].simple == nil || nodes[index].simple.kind != "call" {
t.Fatalf("call statement %d was not parsed into typed call node: %#v", index, nodes[index])
}
if _, ok := nodes[index].simple.expression.(callExpression); !ok {
t.Fatalf("call statement expression = %T, want callExpression", nodes[index].simple.expression)
}
}
output, err := runTest(t, source, "")
if err != nil {
t.Fatal(err)
}
if got, want := output, "5\n5\n"; got != want {
t.Fatalf("Call/Run output = %q, want %q", got, want)
}
}

func TestRememberFieldStatementsUseTypedASTNodesAndExecute(t *testing.T) {
nodes, err := parse("Remember value as this.name.", "remember.tell")
if err != nil {
Expand Down
31 changes: 31 additions & 0 deletions tell/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,35 @@ def parsed_expression(expression: str) -> TellExpr | None:



for prefix in ("call ", "run "):
rest = _take_prefix(text, prefix)
if rest is None:
continue
name, tail = _first_identifier(rest)
if not name or not re.fullmatch(r"(?:[^\W\d]|_)\w*", name):
continue
arguments = []
valid = True
if tail:
argument_text = _take_prefix(tail, "with ")
if not argument_text:
valid = False
else:
try:
arguments = [
parse_expression(_translate_expression_text(argument))
for argument in _split_top_level_text(argument_text, " and ")
]
except ExpressionSyntaxError:
valid = False
if valid:
callee = TellExpr("name", name)
call = TellExpr(
"call", children=(callee, *arguments),
names=(None,) * len(arguments),
)
return {"kind": "call", "source": text, "expression": call}

remember_rest = _take_prefix(text, "remember ")
remember_parts = _split_once_casefold(remember_rest, " as this.") if remember_rest is not None else None
if remember_parts and re.fullmatch(r"(?:[^\W\d]|_)\w*", remember_parts[1]):
Expand Down Expand Up @@ -1712,6 +1741,8 @@ def execute_node(self, node: Node, env: Environment) -> None:
if not isinstance(target, TellInstance):
raise TellError("Property assignment requires a Tell instance.")
target.fields[simple["target"].value] = value
elif simple["kind"] == "call":
pass
elif simple["kind"] == "remember_field":
try:
instance = env.get("this")
Expand Down
15 changes: 15 additions & 0 deletions tests/test_tell.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,21 @@ def test_indexed_assignments_are_typed_nodes_and_execute(self):
self.assertEqual(env.get("mapping"), {"name": "updated"})



def test_call_and_run_statements_are_typed_nodes(self):
source = """
To combine with left and right:
Say left plus right.
End function.
Call combine with 2 and 3.
Run combine with 4 and 1.
"""
parsed = Parser(source).parse()
self.assertEqual([parsed[index].data["simple"]["kind"] for index in (1, 2)], ["call", "call"])
self.assertEqual(parsed[1].data["simple"]["expression"].kind, "call")
output, _ = self.run_tell(source)
self.assertEqual(output, ["5", "5"])

def test_remember_field_is_a_typed_node_and_updates_instance(self):
parsed = Parser("Remember value as this.name.").parse()
simple = parsed[0].data["simple"]
Expand Down
Loading