Developer Tools

vtypecheck

A static type checker for Venutian’s type annotations, written in Venutian.

Released v1.0.0 · 2026-09-28
Import
use "stdlib/typecheck.ven"

Checks the type annotations in Venutian source against its syntax tree, written in Venutian itself.

vtypecheck.ven
print is_declaration_node_type("ShapeDecl")
:: yes
Reference (43 functions)

API reference

new_check_result()

A fresh, empty check report: no errors, no warnings, "ok" yet.

r = new_check_result()
print(map_get(r, "ok"))
:: prints: yes

add_error(result, line, code, message)

Records one hard TYPE-CHECK error against result and flips it to not-ok.

r = new_check_result()
add_error(r, 3, "arg-count", "'f' called with 1 argument(s), expected 2")
print(map_get(r, "ok"))
:: prints: no

add_warning(result, line, code, message)

Records one LINT warning against result without changing its pass/fail verdict.

r = new_check_result()
add_warning(r, 7, "unreachable-code", "this can never run")
print(map_get(r, "ok"))
:: prints: yes

Declaration collection (top-level make/tool make/shape only)

param_info(name, type_name, has_default)

Builds the one small record shape this whole module uses for both a function parameter and a shape field.

p = param_info("age", "Integer", no)
print(map_get(p, "type_name"))
:: prints: Integer

collect_params(param_nodes)

Turns a FunctionDecl/BehaviorDecl method's raw AST parameter list into a List of param_info Maps.

decls = collect_declarations(parse_to_ast("make add(a, b: Integer)\n    give a + b\n"))
params = map_get(map_get(decls, "functions"), "add")
print(length(params))
:: prints: 2

collect_fields(field_nodes)

Turns a ShapeDecl's raw AST field list into a List of param_info Maps, the same shape collect_params produces for a function's parameters, so a shape constructor call is checked by the identical arity/type logic a plain function call is.

decls = collect_declarations(parse_to_ast("shape Point\n    x: Integer\n    y: Integer\n"))
fields = map_get(map_get(decls, "shapes"), "Point")
print(length(fields))
:: prints: 2

collect_methods_for_shape(behavior_stmt)

Builds one BehaviorDecl's own method table: a Map from each declared method's name to its argument-facing parameter list.

source = "shape Dog\n    name: Text\nbehavior Dog\n    make bark(self, times: Integer)\n        give nothing\n"
decls = collect_declarations(parse_to_ast(source))
methods = map_get(map_get(decls, "methods"), "Dog")
print(length(map_get(methods, "bark")))
:: prints: 1

add_behavior_methods(methods_map, behavior_stmt)

Folds one BehaviorDecl into the shared shape-name-to-methods table, in place.

methods = new_map()
source = "shape Dog\n    name: Text\nbehavior Dog\n    make bark(self)\n        give nothing\n"
statements = map_get(parse_to_ast(source), "statements")
add_behavior_methods(methods, statements[1])
print(map_has(methods, "Dog"))
:: prints: yes

collect_declarations(program)

decls = collect_declarations(parse_to_ast("make double(n)\n    give n * 2\n"))
print(map_has(map_get(decls, "functions"), "double"))
:: prints: yes

Cross-file resolution (use "other.ven")

is_name_assignment(stmt)

True when stmt is a plain NAME = expr module-state assignment: an AssignStatement whose own target is a bare Name (not an Index or FieldAccess target).

statements = map_get(parse_to_ast("x = 5\n"), "statements")
print(is_name_assignment(statements[0]))
:: prints: yes

is_declaration_node_type(t)

print(is_declaration_node_type("ShapeDecl"))
:: prints: yes

exportable_statements(statements)

Picks out every declaration a used file makes plain-name-resolvable to whatever 'use "other.ven"'s it, honoring the same leading-run cutoff and expose visibility rule the real compiler applies.

statements = map_get(parse_to_ast("make helper()\n    give 1\n"), "statements")
print(length(exportable_statements(statements)))
:: prints: 1

apply_expose_visibility(decls)

Narrows a used file's own declarations to just the exposed ones, when it uses expose at all.

statements = map_get(parse_to_ast("expose make shown()\n    give 1\nmake hidden()\n    give 2\n"), "statements")
print(length(apply_expose_visibility(statements)))
:: prints: 1

merge_exported_decl(d, show_names, decls)

Folds one already-exportable declaration from a used file into this check's own functions/shapes/methods lookup tables, in place.

decls = collect_declarations(parse_to_ast(""))
lib_decl = map_get(parse_to_ast("make helper(x)\n    give x\n"), "statements")[0]
merge_exported_decl(lib_decl, nothing, decls)
print(map_has(map_get(decls, "functions"), "helper"))
:: prints: yes

merge_use_statement(stmt, base_dir, decls, visited)

Resolves and merges one plain (unaliased) top-level use "other.ven" statement into decls, reading and parsing the target file itself.

decls = collect_declarations(parse_to_ast("use \"vtypecheck.ven\"\n"))
visited = new_map()
use_stmt = map_get(parse_to_ast("use \"vtypecheck.ven\"\n"), "statements")[0]
merge_use_statement(use_stmt, "stdlib", decls, visited)
print(map_has(map_get(decls, "functions"), "new_check_result"))
:: prints: yes

gather_use_declarations(statements, base_dir, decls)

The cross-file entry point: resolves every direct, plain (unaliased) top-level use in statements and merges each one's exports into decls, mutating it in place.

decls = collect_declarations(parse_to_ast("use \"vtypecheck.ven\"\n"))
statements = map_get(parse_to_ast("use \"vtypecheck.ven\"\n"), "statements")
gather_use_declarations(statements, "stdlib", decls)
print(map_has(map_get(decls, "functions"), "add_error"))
:: prints: yes

Statically-certain type of an expression

literal_kind_for(node_type_name)

print(literal_kind_for("TextLiteral"))
:: prints: Text

certain_type_of(expr_node, decls, scope)

The heart of this module's whole "never claim a type it cannot prove" discipline: works out the statically-certain type of one expression, or gives back nothing when it can't be sure.

decls = collect_declarations(parse_to_ast(""))
scope = new_map()
literal = map_get(parse_to_ast("5\n"), "statements")[0]
print(certain_type_of(map_get(literal, "expr"), decls, scope))
:: prints: Integer

field_type_lookup(fields, field_name)

Looks up one field's own declared type annotation by name in a shape's field list.

decls = collect_declarations(parse_to_ast("shape Point\n    x: Integer\n    y: Integer\n"))
fields = map_get(map_get(decls, "shapes"), "Point")
print(field_type_lookup(fields, "x"))
:: prints: Integer

param_index_of(params, param_name)

Finds a named parameter's own position among a callee's declared parameters.

decls = collect_declarations(parse_to_ast("make greet(name, times)\n    give nothing\n"))
params = map_get(map_get(decls, "functions"), "greet")
print(param_index_of(params, "times"))
:: prints: 1

types_compatible(certain, annotated)

Decides whether a statically-certain argument type satisfies a parameter's declared annotation.

print(types_compatible("Integer", "Number"))
:: prints: yes

Walking a call site

check_call(call_node, decls, scope, result)

program = parse_to_ast("make f(a)\n    give a\nf(1, 2)\n")
result = check_program(program)
print(map_get(result, "ok"))
:: prints: no

check_name_call(call_node, callee, decls, scope, result)

program = parse_to_ast("shape Point\n    x: Integer\n    y: Integer\nPoint(1)\n")
result = check_program(program)
print(map_get(result, "ok"))
:: prints: no

check_method_call(call_node, callee, decls, scope, result)

Checks a method call, obj.method(x), but only when objs own shape type can be proven statically certain and that shape declares a matching method.

source = "shape Dog\n    name: Text\nbehavior Dog\n    make bark(self, times: Integer)\n        give nothing\nd = Dog(\"Rex\")\nd.bark()\n"
result = check_program(parse_to_ast(source))
print(map_get(result, "ok"))
:: prints: no

check_call_against_params(call_node, name, target_params, decls, scope, result)

The shared arity, keyword-name, and argument-type checking rules applied to one call, once its callee has already been resolved to a declared parameter/field list (by either check_name_call or check_method_call).

program = parse_to_ast("make greet(name, times)\n    give nothing\ngreet(name: \"a\")\n")
result = check_program(program)
print(map_get(result, "ok"))
:: prints: no

Generic expression walker (finds every reachable Call)

walk_expr(node, decls, scope, result)

program = parse_to_ast("make f(a)\n    give a\nprint(1 + f(1, 2))\n")
result = check_program(program)
print(map_get(result, "ok"))
:: prints: no

Statement walker + local-variable literal-type tracking

alias_pair(a, b, line)

Builds one tracked aliasing record: two local names known to share the exact same underlying object, plus the line the alias was created on (for the eventual warning message).

pair = alias_pair("root", "node", 4)
print(map_get(pair, "line"))
:: prints: 4

alias_remove_involving(pairs, name)

Drops every tracked alias pair that involves name from either side.

pairs = [alias_pair("root", "node", 4)]
print(length(alias_remove_involving(pairs, "node")))
:: prints: 0

alias_find_partner(pairs, name)

Finds the tracked alias pair involving name, if any.

pairs = [alias_pair("root", "node", 4)]
found = alias_find_partner(pairs, "node")
print(map_get(found, "a"))
:: prints: root

alias_other(pair, name)

Given an alias pair and one of its two names, gives back the other one.

pair = alias_pair("root", "node", 4)
print(alias_other(pair, "node"))
:: prints: root

check_aliasing_hazard(stmt, t, alias_pairs, result)

The ALIASING-HAZARD lint rule: tracks the sound, narrow b = a aliasing shape Venutian's reference semantics actually guarantees, and warns the moment either name is later mutated in place while still aliased.

source = "make f()\n    a = [1]\n    b = a\n    b[0] = 2\n"
result = check_program(parse_to_ast(source))
print(length(map_get(result, "warnings")))
:: prints: 1

node_contains_emit(node)

A generic, sound, no-false-negative deep scan for any EmitStatement/EmitExpr anywhere inside an AST subtree, at any depth.

statements = map_get(parse_to_ast("make f()\n    when yes\n        emit 1\n"), "statements")
body = map_get(statements[0], "body")
print(node_contains_emit(body))
:: prints: yes

check_generator_protect_hazard(stmt, t, result)

The GENERATOR-PROTECT-HAZARD lint rule: warns when a protect/with block's body textually contains an emit, since an abandoned generator suspended inside such a block can leave its lock/resource held until the host garbage collector eventually reclaims it.

source = "share lock = 0\nmake f()\n    protect lock\n        emit 1\n"
result = check_program(parse_to_ast(source))
print(length(map_get(result, "warnings")))
:: prints: 1

is_alias_tracking_safe_statement(t)

The complete, default-deny allow-list of statement kinds proven never to rebind a plain local Name, so check_aliasing_hazard knows tracked aliasing can safely survive one of these unchanged.

print(is_alias_tracking_safe_statement("AssignStatement"))
:: prints: no

walk_body(statements, decls, scope, result)

Walks one straight line List of statements, checking every statement in turn, tracking aliasing hazards across them, and running the DEAD-CODE lint rule: a statement immediately following an unconditional give/raise in this SAME list can never run.

source = "make f()\n    give 1\n    print(\"dead\")\n"
result = check_program(parse_to_ast(source))
print(length(map_get(result, "warnings")))
:: prints: 1

walk_statement(stmt, decls, scope, result)

Checks one statement, recursing into its own sub-expressions and nested bodies, and updates scopes local-variable literal-type tracking for a plain name = expr assignment.

source = "make f(n: Text)\n    x = 5\n    print(n + x)\n"
result = check_program(parse_to_ast(source))
print(map_get(result, "ok"))
:: prints: yes

Entry points per declared function/method

scope_for_params(params, self_type)

Builds the initial local-variable-to-type scope for one function or method body, seeded from its own parameter annotations.

decls = collect_declarations(parse_to_ast("make f(n: Integer)\n    give n\n"))
params = map_get(map_get(decls, "functions"), "f")
scope = scope_for_params(params, nothing)
print(map_get(scope, "n"))
:: prints: Integer

check_function_body(decl_node, decls, result, self_type)

Checks one function or method's own body: collects its parameters, seeds a fresh scope from them, and walks its statements.

decl = map_get(parse_to_ast("make f()\n    g()\n"), "statements")[0]
decls = collect_declarations(parse_to_ast("make f()\n    g()\n"))
result = new_check_result()
check_function_body(decl, decls, result, nothing)
print(map_get(result, "ok"))
:: prints: yes

check_program(program)

Type-checks an already-parsed program with no known file path, so cross-file use resolution is skipped.

result = check_program(parse_to_ast("make f(n: Integer)\n    give n\nf(1)\n"))
print(map_get(result, "ok"))
:: prints: yes

check_program_at(program, base_dir)

result = check_program_at(parse_to_ast("make f(n: Integer)\n    give n\nf(\"x\")\n"), nothing)
print(map_get(result, "ok"))
:: prints: no

check_source(source_text)

Type-checks raw Venutian source text with no file path, so a plain use "other.ven" inside it can never be resolved.

result = check_source("make f(n)\n    give n\nf(1, 2)\n")
print(map_get(result, "ok"))
:: prints: no

check_file(path)

Type-checks a real .ven file on disk, with full cross-file use resolution.

result = check_file("stdlib/typecheck.ven")
print(kind(result))
:: prints: Map

Human-readable report

format_check_report(file_label, result)

Renders a check-result Map into the plain-text report Venutian's own CLI prints for venutian check.

result = check_source("make f(n)\n    give n\nf()\n")
print(format_check_report("example.ven", result))
:: prints: example.ven: 1 type error(s) found
:: prints:   line 3: 'f' called with 0 argument(s), expected 1

Get notified when vtypecheck changes

Get notified about

New versions and security advisories.

New and updated packages, once a day.

News on the math model built in Venutian.