Developer Tools
vtypecheck
A static type checker for Venutian’s type annotations, written in Venutian.
Released v1.0.0 · 2026-09-28use "stdlib/typecheck.ven"
Checks the type annotations in Venutian source against its syntax tree, written in Venutian itself.
print is_declaration_node_type("ShapeDecl")
:: yes
Reference (43 functions)
- Functions
- Declaration collection (top-level 'make'/'tool make'/'shape' only)
- Cross-file resolution ('use "other.ven"')
- Statically-certain type of an expression
- Walking a call site
- Generic expression walker (finds every reachable Call)
- Statement walker + local-variable literal-type tracking
- Entry points per declared function/method
- Human-readable report
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: yesadd_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: noadd_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: yesDeclaration 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: Integercollect_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: 2collect_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: 2collect_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: 1add_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: yescollect_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: yesCross-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: yesis_declaration_node_type(t)
print(is_declaration_node_type("ShapeDecl"))
:: prints: yesexportable_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: 1apply_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: 1merge_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: yesmerge_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: yesgather_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: yesStatically-certain type of an expression
literal_kind_for(node_type_name)
print(literal_kind_for("TextLiteral"))
:: prints: Textcertain_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: Integerfield_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: Integerparam_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: 1types_compatible(certain, annotated)
Decides whether a statically-certain argument type satisfies a parameter's declared annotation.
print(types_compatible("Integer", "Number"))
:: prints: yesWalking 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: nocheck_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: nocheck_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: nocheck_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: noGeneric 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: noStatement 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: 4alias_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: 0alias_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: rootalias_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: rootcheck_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: 1node_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: yescheck_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: 1is_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: nowalk_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: 1walk_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: yesEntry 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: Integercheck_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: yescheck_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: yescheck_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: nocheck_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: nocheck_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: MapHuman-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