From feae88c9115b756f17800a17920569f06262f92b Mon Sep 17 00:00:00 2001 From: KernelGhost-gif Date: Thu, 12 Nov 2026 20:38:36 +0530 Subject: [PATCH 01/11] Parse Call and Run function statements into native call AST nodes --- native/internal/tell/parser.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/native/internal/tell/parser.go b/native/internal/tell/parser.go index 2471475..a867efe 100644 --- a/native/internal/tell/parser.go +++ b/native/internal/tell/parser.go @@ -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) { From 7dabe23b8fa406378c8027685aed9373397ea3ca Mon Sep 17 00:00:00 2001 From: KernelGhost-gif Date: Thu, 12 Nov 2026 20:38:44 +0530 Subject: [PATCH 02/11] Execute typed Call and Run statements without echoing return values --- native/internal/tell/execute.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/native/internal/tell/execute.go b/native/internal/tell/execute.go index 1d3f31e..20baa64 100644 --- a/native/internal/tell/execute.go +++ b/native/internal/tell/execute.go @@ -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) From 93cc9eb33efbcb1db3906c7d94c49ca4328ec80f Mon Sep 17 00:00:00 2001 From: KernelGhost-gif Date: Thu, 12 Nov 2026 20:38:47 +0530 Subject: [PATCH 03/11] Discard return values from compiled Call and Run statements --- native/internal/tell/bytecode.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/native/internal/tell/bytecode.go b/native/internal/tell/bytecode.go index b0b1d37..4dea1ec 100644 --- a/native/internal/tell/bytecode.go +++ b/native/internal/tell/bytecode.go @@ -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") From 1cf1d4037e2cceed08759275046ec9e68b4b66ed Mon Sep 17 00:00:00 2001 From: KernelGhost-gif Date: Thu, 12 Nov 2026 20:39:05 +0530 Subject: [PATCH 04/11] Parse and execute Python Call and Run statements as call AST nodes --- tell/interpreter.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tell/interpreter.py b/tell/interpreter.py index 8057feb..e5c8092 100644 --- a/tell/interpreter.py +++ b/tell/interpreter.py @@ -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]): @@ -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") From 135ad8b3daff65ba4d8327de3242d35ca601869d Mon Sep 17 00:00:00 2001 From: KernelGhost-gif Date: Thu, 12 Nov 2026 20:39:50 +0530 Subject: [PATCH 05/11] Test native Call and Run statement AST nodes --- native/internal/tell/tell_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/native/internal/tell/tell_test.go b/native/internal/tell/tell_test.go index 1415edb..71c2f83 100644 --- a/native/internal/tell/tell_test.go +++ b/native/internal/tell/tell_test.go @@ -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 { From fcffe7ed906aedad5a0e68f70dc50e3c77da526b Mon Sep 17 00:00:00 2001 From: KernelGhost-gif Date: Thu, 12 Nov 2026 20:39:54 +0530 Subject: [PATCH 06/11] Test compiled Call and Run statement lowering --- native/internal/tell/bytecode_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/native/internal/tell/bytecode_test.go b/native/internal/tell/bytecode_test.go index 1ab71d8..a2e3018 100644 --- a/native/internal/tell/bytecode_test.go +++ b/native/internal/tell/bytecode_test.go @@ -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: From 2a75cbbda46c22cff696a8bb3f569cc6fd08f4e9 Mon Sep 17 00:00:00 2001 From: KernelGhost-gif Date: Thu, 12 Nov 2026 20:40:02 +0530 Subject: [PATCH 07/11] Test Python Call and Run AST nodes and execution --- tests/test_tell.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_tell.py b/tests/test_tell.py index 548409b..d25b7aa 100644 --- a/tests/test_tell.py +++ b/tests/test_tell.py @@ -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"] From 162b3cb3a07ba35e12779544d4488759327bdac9 Mon Sep 17 00:00:00 2001 From: KernelGhost-gif Date: Thu, 12 Nov 2026 20:40:13 +0530 Subject: [PATCH 08/11] Add shared conformance coverage for Call and Run statements --- conformance/core.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/conformance/core.json b/conformance/core.json index 4162daf..138d4de 100644 --- a/conformance/core.json +++ b/conformance/core.json @@ -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" } ] From 463545027b1178bf80f5298e94e4866ad491e26f Mon Sep 17 00:00:00 2001 From: KernelGhost-gif Date: Thu, 12 Nov 2026 20:40:17 +0530 Subject: [PATCH 09/11] Document Call and Run statement grammar --- docs/GRAMMAR_V1.ebnf | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/GRAMMAR_V1.ebnf b/docs/GRAMMAR_V1.ebnf index c4bb19f..ef65400 100644 --- a/docs/GRAMMAR_V1.ebnf +++ b/docs/GRAMMAR_V1.ebnf @@ -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 ], "." From bd5360a5db9e59b7f084000c8ea28d585ccc7912 Mon Sep 17 00:00:00 2001 From: KernelGhost-gif Date: Thu, 12 Nov 2026 20:40:22 +0530 Subject: [PATCH 10/11] Document typed Call and Run statement architecture --- docs/ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index bf1445a..d3cc0cb 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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: From 2f98c78d3d19f54da9b64800f0f715dc91fb628f Mon Sep 17 00:00:00 2001 From: KernelGhost-gif Date: Thu, 12 Nov 2026 20:40:25 +0530 Subject: [PATCH 11/11] Record typed Call and Run statement migration --- docs/PROGRESS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 7803750..679bbc2 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -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.