Skip to content

AST editing

How Empryo edits TypeScript and JavaScript by symbol name instead of by matching text, so edits don't fail on whitespace or drift with line numbers.

Tools4 minutes to read
Copy & share
On this page6 sections

When Empryo edits a TypeScript or JavaScript file, it changes the code by symbol, not by searching for text. It says "set the return type of fetchUser" instead of "find these exact three lines and replace them". Small changes stay small, and an edit can't fail because of whitespace, quotes or line endings.

The tool that does this is called ast_edit. AST stands for abstract syntax tree, the structured form of your code that the TypeScript compiler builds. You never call ast_edit yourself. Empryo uses it automatically for TypeScript and JavaScript files, and this page is a reference for what it can do.

In a project with no TypeScript or JavaScript files, such as a Go or Rust repository, Empryo loads ast_edit on demand instead of sending it with every request. The model can still pull it in when it meets a stray .ts file. Empryo decides when a tab starts, from the files git tracks plus new files that are not ignored, or from a scan of the folder when it is not a git repository. The decision is saved with the session, so a resumed session keeps the same tools and its prompt cache.

Where the idea comes fromLink to this section

The approach grew out of a Master's thesis in Computer Science, *Typed vs Untyped Programming Languages* (Ouail Bni and Artur Matusiak, 2022), and its reference implementation JS Typer. The thesis showed that converting JavaScript to TypeScript could be done entirely by editing the syntax tree rather than the text. ast_edit applies the same idea to AI coding.

ExamplesLink to this section

Change a function's return type:

ts
ast_edit({
  path: "src/api.ts",
  action: "set_return_type",
  target: "function",
  name: "fetchUser",
  value: "Promise",
})

Make a function async, add a parameter and add an import, as one batch:

ts
ast_edit({
  path: "src/api.ts",
  operations: [
    { action: "set_async", target: "function", name: "fetchUser", value: "true" },
    { action: "add_parameter", target: "function", name: "fetchUser", value: "cache: boolean" },
    { action: "add_named_import", value: "./types", newCode: "User" },
  ],
})

Rename a class method:

ts
ast_edit({
  path: "src/store.ts",
  action: "rename",
  target: "method",
  name: "UserStore.fetch",
  value: "load",
})

Replace a whole function body:

ts
ast_edit({
  path: "src/util.ts",
  action: "set_body",
  target: "function",
  name: "parseInput",
  newCode: "return JSON.parse(raw);",
})

A batch is all-or-nothing. If one operation fails, none of them are applied.

TargetsLink to this section

You point an edit at a symbol by its kind and name.

TargetAddressed by
function, class, interface, type, enum, variable, constantname
method, property"ClassName.member" or "member"
arrow_functionname (for const fn = () => {})
constructorClassName

Anonymous code, such as an inline callback, can't be targeted directly. Empryo edits the named symbol that contains it instead.

Action referenceLink to this section

Small edits
ActionEffect
set_typeType annotation of a variable, property or parameter
set_return_typeFunction return type
set_initializer / remove_initializerVariable initializer
set_valueEnum member value
set_async / set_generatorToggle async / function*
set_export / set_default_exportToggle export modifiers
set_abstract / set_static / set_readonly / set_scope / set_optional / set_overrides / set_ambientClass member modifiers
set_const_enum / set_declaration_kindOther modifiers
renameRename the declaration only
removeDelete the declaration
add_parameter / remove_parameterFunction and method parameters
Body and member edits
ActionEffect
set_bodyReplace a function or method body
add_statement / insert_statement / remove_statementIndividual statements
add_property / remove_propertyInterface and class properties
add_method / remove_method / add_constructor / add_getter / add_setterClass members
add_member / remove_memberEnum and type-literal members
add_decorator / remove_decoratorDecorators
add_overloadFunction overload signatures
set_extends / add_extends / remove_extends / add_implements / remove_implementsInheritance
add_type_parameterGeneric type parameters
add_jsdoc / remove_jsdocJSDoc comments
unwrap, set_structure, extract_interfaceStructural rewrites
Whole-symbol replacement
ActionEffect
replaceReplace a whole symbol
replace_in_bodyReplace text inside a named symbol, found by a unique anchor or a start/end anchor pair
File-level
ActionEffect
create_fileCreate a new file (newCode is the content)
add_imports / add_import / remove_import / add_named_import / remove_named_importImports
set_module_specifierChange an import path
add_export_declaration / add_named_reexportRe-exports
add_namespaceNamespace declarations
organize_imports / fix_missing_imports / fix_unusedCleanup
add_function / add_class / add_interface / add_type_alias / add_enum / add_variableAdd top-level declarations
insert_textInsert text at an anchor ("after-imports", "before-exports", or an index)
rename_globalRename a symbol across the project

Supported filesLink to this section

.ts, .tsx, .js, .jsx, .mts, .cts, .mjs, .cjs. For every other file type, and for files with a syntax error, Empryo uses its regular text-based edit tools.

SafetyLink to this section

  • Batches are atomic: all operations apply, or none do.
  • undo_edit reverses an ast_edit change the same way it reverses any other edit.
  • After an edit, new type errors are reported back to the agent so it can fix them in the same turn.
  • Files blocked by your /privacy patterns can't be edited.