summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-08-10 11:11:18 -0700
committerCliff Click <cliffc@acm.org>2026-11-12 23:28:58 -0800
commit772e3a1036d571de55b178ebd6d11ee5140d4e36 (patch)
treef81fd53601df21871adb7500f8dc2b8fa3a829a2
parent15b634f47b300cb611b30554ce390ac20f149f8f (diff)
downloadSimple-772e3a1036d571de55b178ebd6d11ee5140d4e36.tar.gz
Simple-772e3a1036d571de55b178ebd6d11ee5140d4e36.zip
Chapter 29: Tech Debt Cleanup
Co-authored-by: Hsiang-Ying Fu <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: fuhsnn <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: Symious <14933944+symious@users.noreply.github.com> Co-authored-by: Jun Kawasaki <jun784@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--README.ja.md2
-rw-r--r--README.md2
-rw-r--r--chapter29.iml (renamed from chapter28b.iml)4
-rw-r--r--docs/chapter29/29-grammar.ja.md181
-rw-r--r--docs/chapter29/29-grammar.md239
-rw-r--r--docs/chapter29/README.ja.md39
-rw-r--r--docs/chapter29/README.md155
-rw-r--r--docs/chapter29/construction-groups.md241
-rw-r--r--docs/chapter29/deferred-construction.md385
-rw-r--r--docs/chapter29/escape-flow.md147
-rw-r--r--src/main/java/com/seaofnodes/simple/Parser.java3200
-rw-r--r--src/main/java/com/seaofnodes/simple/README.md15
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/ClassInitDependencies.java13
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/CodeGen.java14
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/CompUnit.java3
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/Encoding.java5
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/Iter.java40
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/IterPeeps.java (renamed from src/main/java/com/seaofnodes/simple/IterPeeps.java)16
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/ListScheduler.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/Opto.java69
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/ParseAll.java66
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java19
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/Serialize.java19
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/StaticData.java8
-rw-r--r--src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java37
-rw-r--r--src/main/java/com/seaofnodes/simple/node/AndNode.java5
-rw-r--r--src/main/java/com/seaofnodes/simple/node/ArithNode.java24
-rw-r--r--src/main/java/com/seaofnodes/simple/node/BoolNode.java20
-rw-r--r--src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/node/CFGNode.java1
-rw-r--r--src/main/java/com/seaofnodes/simple/node/CLInitNode.java20
-rw-r--r--src/main/java/com/seaofnodes/simple/node/CallEndNode.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/node/CallNode.java25
-rw-r--r--src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java18
-rw-r--r--src/main/java/com/seaofnodes/simple/node/ConstructNode.java77
-rw-r--r--src/main/java/com/seaofnodes/simple/node/ConvertNode.java27
-rw-r--r--src/main/java/com/seaofnodes/simple/node/CtrlNode.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/node/EscapeNode.java22
-rw-r--r--src/main/java/com/seaofnodes/simple/node/FRefNode.java13
-rw-r--r--src/main/java/com/seaofnodes/simple/node/FldOffNode.java13
-rw-r--r--src/main/java/com/seaofnodes/simple/node/FunNode.java26
-rw-r--r--src/main/java/com/seaofnodes/simple/node/InitViewNode.java30
-rw-r--r--src/main/java/com/seaofnodes/simple/node/LoadNode.java47
-rw-r--r--src/main/java/com/seaofnodes/simple/node/LoopNode.java5
-rw-r--r--src/main/java/com/seaofnodes/simple/node/MemMergeNode.java1
-rw-r--r--src/main/java/com/seaofnodes/simple/node/MemOpNode.java12
-rw-r--r--src/main/java/com/seaofnodes/simple/node/MinusNode.java6
-rw-r--r--src/main/java/com/seaofnodes/simple/node/MulNode.java8
-rw-r--r--src/main/java/com/seaofnodes/simple/node/NeverNode.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/node/NewNode.java8
-rw-r--r--src/main/java/com/seaofnodes/simple/node/Node.java10
-rw-r--r--src/main/java/com/seaofnodes/simple/node/NotNode.java3
-rw-r--r--src/main/java/com/seaofnodes/simple/node/OrNode.java5
-rw-r--r--src/main/java/com/seaofnodes/simple/node/ParmNode.java13
-rw-r--r--src/main/java/com/seaofnodes/simple/node/PhiNode.java57
-rw-r--r--src/main/java/com/seaofnodes/simple/node/PtrToIntNode.java4
-rw-r--r--src/main/java/com/seaofnodes/simple/node/RegionNode.java11
-rw-r--r--src/main/java/com/seaofnodes/simple/node/ReturnNode.java18
-rw-r--r--src/main/java/com/seaofnodes/simple/node/SarNode.java5
-rw-r--r--src/main/java/com/seaofnodes/simple/node/ScopeNode.java21
-rw-r--r--src/main/java/com/seaofnodes/simple/node/ShlNode.java5
-rw-r--r--src/main/java/com/seaofnodes/simple/node/ShrNode.java5
-rw-r--r--src/main/java/com/seaofnodes/simple/node/StartNode.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/node/StoreNode.java32
-rw-r--r--src/main/java/com/seaofnodes/simple/node/SubNode.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/node/ToFloatNode.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/node/ToIntegerNode.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/node/TypeNode.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/node/XCtrlNode.java2
-rw-r--r--src/main/java/com/seaofnodes/simple/node/XorNode.java5
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Allocations.java214
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Construction.java130
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/ControlFlow.java96
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Conversions.java72
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Declarations.java164
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Expressions.java394
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Functions.java349
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Lexer.java389
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Parser.java281
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/README.md36
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/References.java315
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Statements.java272
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Structs.java338
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Types.java245
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/Var.java (renamed from src/main/java/com/seaofnodes/simple/Var.java)15
-rw-r--r--src/main/java/com/seaofnodes/simple/parser/package-info.java14
-rw-r--r--src/main/java/com/seaofnodes/simple/type/Type.java1
-rw-r--r--src/main/java/com/seaofnodes/simple/type/TypeMem.java9
-rw-r--r--src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java79
-rw-r--r--src/main/java/com/seaofnodes/simple/type/TypeStruct.java43
-rw-r--r--src/test/java/com/seaofnodes/simple/ArithNodeTest.java32
-rw-r--r--src/test/java/com/seaofnodes/simple/BoundaryTest.java6
-rw-r--r--src/test/java/com/seaofnodes/simple/CallEndTest.java54
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter10Test.java1
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter12Test.java13
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter17bTest.java13
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter18Test.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter19Test.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter20Test.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter22Test.java1
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter25Test.java1
-rw-r--r--src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java1
-rw-r--r--src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java1
-rw-r--r--src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java162
-rw-r--r--src/test/java/com/seaofnodes/simple/ConstructionRequirementsTest.java222
-rw-r--r--src/test/java/com/seaofnodes/simple/ConstructorCompletionTest.java119
-rw-r--r--src/test/java/com/seaofnodes/simple/Eval2.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/FinalConstructionTest.java10
-rw-r--r--src/test/java/com/seaofnodes/simple/ParserTest.java33
-rw-r--r--src/test/java/com/seaofnodes/simple/PartialReadTest.java155
-rw-r--r--src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java1
-rw-r--r--src/test/java/com/seaofnodes/simple/TypeTest.java24
-rw-r--r--src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java25
-rw-r--r--src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java4
117 files changed, 6290 insertions, 3601 deletions
diff --git a/README.ja.md b/README.ja.md
index ca116940..1081089f 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -68,7 +68,7 @@ Simple 言語のスタイルは、C または Java のサブセットに倣っã�
* [第27章](docs/chapter27/README.ja.md):エスケープ解析と未知の呼び出し元
* [第28a章](docs/chapter28a/README.ja.md):モジュールと遅延クラス初期化
* [第28b章](docs/chapter28b/README.ja.md):分離コンパイル
-* 第29章:技術的負債の整理
+* [第29章](docs/chapter29/README.ja.md):技術的負債の整理
* 第30章:テキスト整形と診断
* 第31章:ソフトウェア描画とグラフィカルゲーム(作業中)
diff --git a/README.md b/README.md
index 594c19a0..30336e77 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,7 @@ The Simple language is styled after a subset of C or Java.
* [Chapter 27](docs/chapter27/README.md): Escape Analysis and Unknown Callers
* [Chapter 28a](docs/chapter28a/README.md): Modules and Lazy Class Initialization
* [Chapter 28b](docs/chapter28b/README.md): Separate Compilation
-* Chapter 29: Tech Debt Cleanup, Partial Objects
+* [Chapter 29](docs/chapter29/README.md): Tech Debt Cleanup, Partial Objects
* Chapter 30: Text Formatting and Diagnostics
* Chapter 31: Software Drawing and Graphical Games (work in progress)
diff --git a/chapter28b.iml b/chapter29.iml
index aa7365c4..7905a62c 100644
--- a/chapter28b.iml
+++ b/chapter29.iml
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="false">
- <output url="file://$MODULE_DIR$/../build/idea/production/chapter28b" />
- <output-test url="file://$MODULE_DIR$/../build/idea/test/chapter28b" />
+ <output url="file://$MODULE_DIR$/../build/idea/production/chapter29" />
+ <output-test url="file://$MODULE_DIR$/../build/idea/test/chapter29" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
diff --git a/docs/chapter29/29-grammar.ja.md b/docs/chapter29/29-grammar.ja.md
new file mode 100644
index 00000000..b7794410
--- /dev/null
+++ b/docs/chapter29/29-grammar.ja.md
@@ -0,0 +1,181 @@
+# 第29章の文法
+
+[English](29-grammar.md) | 日本語
+
+```antlrv4
+grammar SimpleLanguage;
+
+// Name lookup, assignable operands, type compatibility, and access permissions
+// are checked by the compiler in addition to these syntax rules.
+// A file is a compilation unit; its path supplies its namespace.
+// Qualified names resolve nested types and other units without an import keyword.
+program : statement* EOF ;
+
+block : '{' statement* '}' ;
+
+statement
+ : block
+ | returnStatement
+ | ifStatement
+ | whileStatement
+ | forStatement
+ | breakStatement
+ | continueStatement
+ | structDeclaration
+ | declaration ';'
+ | assignment ';'
+ | ';'
+ ;
+
+returnStatement : 'return' assignment ';' ;
+ifStatement : 'if' '(' assignment ')' statement ('else' statement)? ;
+whileStatement : 'while' '(' assignment ')' statement ;
+breakStatement : 'break' ';' ;
+continueStatement : 'continue' ';' ;
+
+forStatement
+ : 'for' '(' (declaration | assignment)? ';' assignment? ';' assignment? ')' statement
+ ;
+
+structDeclaration : 'struct' IDENTIFIER '{' structMember* '}' ';' ;
+structMember : constructorDeclaration | methodDeclaration | statement ;
+
+// The constructor name must match the enclosing struct's name.
+constructorDeclaration : 'new' IDENTIFIER '=' constructorExpression ';' ;
+constructorExpression : '{' constructorParameterList? '->' statement* '}' ;
+constructorParameterList : (constructorParameter ','?)+ ;
+constructorParameter : type '^'? bindingModifier? IDENTIFIER ;
+// ^ accepts an unfinished struct reference; ? independently permits null.
+// Only self may be read while incomplete; ^ arguments may be stored/forwarded.
+// A new result is incomplete iff an actual argument is. See construction-groups.md.
+
+// A method is an ordinary fixed function binding in a struct definition.
+// The compiler supplies self; it is not written in parameterList.
+// Function types and expressions both receive their implicit receiver here.
+methodDeclaration : 'val' bindingModifier? IDENTIFIER '=' functionExpression ';' ;
+
+declaration : declarationType binding (',' binding)* ;
+declarationType : type | 'var' | 'val' ;
+binding : bindingModifier? IDENTIFIER ('=' initializer)? ;
+bindingModifier : '!' | '~' ;
+// A declaration initializer consisting of the string "C" declares an extern
+// symbol: a function, or explicit numeric native storage. Ordinary strings
+// elsewhere are array-valued expressions.
+initializer : assignment ;
+
+// The prefix qualifies the named struct reference, not an enclosing array.
+// Each [] / [~] suffix qualifies its own array layer. ? requires a reference.
+type
+ : functionType
+ | primitiveType typeSuffix*
+ | accessModifier? typeName typeSuffix*
+ ;
+accessModifier : '!' | '~' ;
+typeSuffix : '?' | '[]' | '[~]' ;
+typeName : IDENTIFIER ('.' IDENTIFIER)* ;
+
+primitiveType
+ : 'int' | 'i8' | 'i16' | 'i32' | 'i64'
+ | 'u1' | 'u8' | 'u16' | 'u32' | 'byte' | 'bool'
+ | 'flt' | 'f32' | 'f64'
+ ;
+
+assignment : expression (assignmentOperator assignment)? ;
+assignmentOperator
+ : '=' | '+=' | '-=' | '*=' | '/=' | '&=' | '|=' | '^='
+ | '<<=' | '>>=' | '>>>='
+ ;
+
+expression : logicalExpression ('?' assignment (':' assignment)?)? ;
+
+// Both logical operators short-circuit and associate to the right here.
+logicalExpression : bitwiseExpression (('&&' | '||') logicalExpression)? ;
+
+bitwiseExpression
+ : equalityExpression (('&' | '|' | '^') equalityExpression)*
+ ;
+
+equalityExpression : comparisonExpression (('==' | '!=') comparisonExpression)* ;
+
+// Chained comparisons test adjacent operands and short-circuit. A chain must
+// keep its direction; equality has lower precedence and does not join a chain.
+comparisonExpression
+ : shiftExpression
+ ( (('<' | '<=') shiftExpression)+ | (('>' | '>=') shiftExpression)+ )?
+ ;
+
+shiftExpression
+ : additiveExpression (('<<' | '>>' | '>>>') additiveExpression)*
+ ;
+
+additiveExpression
+ : multiplicativeExpression (('+' | '-') multiplicativeExpression)*
+ ;
+
+multiplicativeExpression
+ : unaryExpression (('*' | '/') unaryExpression)*
+ ;
+
+unaryExpression
+ : ('-' | '!') unaryExpression
+ | updateOperator IDENTIFIER
+ | postfixExpression
+ ;
+
+// Prefix updates require a local name; postfix updates also allow fields
+// and array elements. All updates require an assignable operand.
+updateOperator : '++' | '--' ;
+postfixExpression : primaryExpression postfixSuffix* updateOperator? ;
+postfixSuffix : '.' IDENTIFIER | '[' assignment ']' | '#' | callSuffix | methodCallSuffix ;
+
+// For a fixed function-valued member, object.name(args) passes object as self.
+// This overlaps ordinary field selection followed by a call syntactically;
+// name/type resolution determines which call form applies.
+methodCallSuffix : '.' IDENTIFIER callSuffix ;
+
+primaryExpression
+ : INTEGER_LITERAL
+ | FLOAT_LITERAL
+ | STRING_LITERAL
+ | CHARACTER_LITERAL
+ | 'true'
+ | 'false'
+ | 'null'
+ | IDENTIFIER
+ | '(' assignment ')'
+ | newExpression
+ | functionExpression
+ ;
+
+// A function is an expression bound with an ordinary declaration.
+// Its last statement supplies the implicit result; return can exit earlier.
+functionExpression : '{' parameterList? '->' statement* '}' ;
+parameterList : (parameter ','?)+ ;
+parameter : type bindingModifier? IDENTIFIER ;
+
+// Function types have unnamed, space-separated argument types.
+// {int} is the zero-argument type; its function expression still uses ->.
+functionType
+ : '{' type+ '->' type '}' '?'?
+ | '{' type '}' '?'?
+ ;
+
+callSuffix : '(' argumentList? ')' ;
+argumentList : assignment (',' assignment)* ','? ;
+
+// Allocation invokes the declared constructor; inline initializer blocks from
+// earlier chapters are replaced by constructor arguments.
+newExpression : 'new' type ('[' assignment ']' | callSuffix)? ;
+
+INTEGER_LITERAL : '0' | [1-9] DIGIT* ;
+FLOAT_LITERAL : DIGIT+ ('.' DIGIT* ('e' DIGIT+)? | 'e' '-'? DIGIT+) ;
+STRING_LITERAL : '"' (ESCAPE | ~["\\])* '"' ;
+CHARACTER_LITERAL : '\'' (ESCAPE | ~['\\]) '\'' ;
+fragment ESCAPE : '\\' [0ntr\\'"] ;
+IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ;
+fragment NON_DIGIT : [a-zA-Z_] ;
+fragment DIGIT : [0-9] ;
+WHITESPACE : [ \t\r\n]+ -> skip ;
+LINE_COMMENT : '//' ~[\r\n]* -> skip ;
+BLOCK_COMMENT : '/*' .*? '*/' -> skip ;
+```
diff --git a/docs/chapter29/29-grammar.md b/docs/chapter29/29-grammar.md
new file mode 100644
index 00000000..1a6ab345
--- /dev/null
+++ b/docs/chapter29/29-grammar.md
@@ -0,0 +1,239 @@
+# Grammar for Chapter 29
+
+English | [日本語](29-grammar.ja.md)
+
+The handwritten recursive-descent implementation lives in
+[`com.seaofnodes.simple.parser`](../../src/main/java/com/seaofnodes/simple/parser/).
+The production-to-code map below the grammar describes where syntax ends and
+name resolution or graph construction begins. This grammar documents the
+language; it is not input to a parser generator.
+
+```antlrv4
+grammar SimpleLanguage;
+
+// Name lookup, assignable operands, type compatibility, and access permissions
+// are checked by the compiler in addition to these syntax rules.
+// A file is a compilation unit; its path supplies its namespace.
+// Qualified names resolve nested types and other units without an import keyword.
+program : statement* EOF ;
+
+block : '{' statement* '}' ;
+
+statement
+ : block
+ | returnStatement
+ | ifStatement
+ | whileStatement
+ | forStatement
+ | breakStatement
+ | continueStatement
+ | structDeclaration
+ | declaration ';'
+ | assignment ';'
+ | ';'
+ ;
+
+returnStatement : 'return' assignment ';' ;
+ifStatement : 'if' '(' assignment ')' statement ('else' statement)? ;
+whileStatement : 'while' '(' assignment ')' statement ;
+breakStatement : 'break' ';' ;
+continueStatement : 'continue' ';' ;
+
+forStatement
+ : 'for' '(' (declaration | assignment)? ';' assignment? ';' assignment? ')' statement
+ ;
+
+structDeclaration : 'struct' IDENTIFIER '{' structMember* '}' ';' ;
+structMember : constructorDeclaration | methodDeclaration | statement ;
+
+// The constructor name must match the enclosing struct's name.
+constructorDeclaration : 'new' IDENTIFIER '=' constructorExpression ';' ;
+constructorExpression : '{' constructorParameterList? '->' statement* '}' ;
+constructorParameterList : (constructorParameter ','?)+ ;
+constructorParameter : type '^'? bindingModifier? IDENTIFIER ;
+// ^ accepts an unfinished struct reference; ? independently permits null.
+// Only self may be read while incomplete; ^ arguments may be stored/forwarded.
+// A new result is incomplete iff an actual argument is. See construction-groups.md.
+
+// A method is an ordinary fixed function binding in a struct definition.
+// The compiler supplies self; it is not written in parameterList.
+// Function types and expressions both receive their implicit receiver here.
+methodDeclaration : 'val' bindingModifier? IDENTIFIER '=' functionExpression ';' ;
+
+declaration : declarationType binding (',' binding)* ;
+declarationType : type | 'var' | 'val' ;
+binding : bindingModifier? IDENTIFIER ('=' initializer)? ;
+bindingModifier : '!' | '~' ;
+// A declaration initializer consisting of the string "C" declares an extern
+// symbol: a function, or explicit numeric native storage. Ordinary strings
+// elsewhere are array-valued expressions.
+initializer : assignment ;
+
+// The prefix qualifies the named struct reference, not an enclosing array.
+// Each [] / [~] suffix qualifies its own array layer. ? requires a reference.
+type
+ : functionType
+ | primitiveType typeSuffix*
+ | accessModifier? typeName typeSuffix*
+ ;
+accessModifier : '!' | '~' ;
+typeSuffix : '?' | '[]' | '[~]' ;
+typeName : IDENTIFIER ('.' IDENTIFIER)* ;
+
+primitiveType
+ : 'int' | 'i8' | 'i16' | 'i32' | 'i64'
+ | 'u1' | 'u8' | 'u16' | 'u32' | 'byte' | 'bool'
+ | 'flt' | 'f32' | 'f64'
+ ;
+
+assignment : expression (assignmentOperator assignment)? ;
+assignmentOperator
+ : '=' | '+=' | '-=' | '*=' | '/=' | '&=' | '|=' | '^='
+ | '<<=' | '>>=' | '>>>='
+ ;
+
+expression : logicalExpression ('?' assignment (':' assignment)?)? ;
+
+// Both logical operators short-circuit and associate to the right here.
+logicalExpression : bitwiseExpression (('&&' | '||') logicalExpression)? ;
+
+bitwiseExpression
+ : equalityExpression (('&' | '|' | '^') equalityExpression)*
+ ;
+
+equalityExpression : comparisonExpression (('==' | '!=') comparisonExpression)* ;
+
+// Chained comparisons test adjacent operands and short-circuit. A chain must
+// keep its direction; equality has lower precedence and does not join a chain.
+comparisonExpression
+ : shiftExpression
+ ( (('<' | '<=') shiftExpression)+ | (('>' | '>=') shiftExpression)+ )?
+ ;
+
+shiftExpression
+ : additiveExpression (('<<' | '>>' | '>>>') additiveExpression)*
+ ;
+
+additiveExpression
+ : multiplicativeExpression (('+' | '-') multiplicativeExpression)*
+ ;
+
+multiplicativeExpression
+ : unaryExpression (('*' | '/') unaryExpression)*
+ ;
+
+unaryExpression
+ : ('-' | '!') unaryExpression
+ | updateOperator IDENTIFIER
+ | postfixExpression
+ ;
+
+// Prefix updates require a local name; postfix updates also allow fields
+// and array elements. All updates require an assignable operand.
+updateOperator : '++' | '--' ;
+postfixExpression : primaryExpression postfixSuffix* updateOperator? ;
+postfixSuffix : '.' IDENTIFIER | '[' assignment ']' | '#' | callSuffix | methodCallSuffix ;
+
+// For a fixed function-valued member, object.name(args) passes object as self.
+// This overlaps ordinary field selection followed by a call syntactically;
+// name/type resolution determines which call form applies.
+methodCallSuffix : '.' IDENTIFIER callSuffix ;
+
+primaryExpression
+ : INTEGER_LITERAL
+ | FLOAT_LITERAL
+ | STRING_LITERAL
+ | CHARACTER_LITERAL
+ | 'true'
+ | 'false'
+ | 'null'
+ | IDENTIFIER
+ | '(' assignment ')'
+ | newExpression
+ | functionExpression
+ ;
+
+// A function is an expression bound with an ordinary declaration.
+// Its last statement supplies the implicit result; return can exit earlier.
+functionExpression : '{' parameterList? '->' statement* '}' ;
+parameterList : (parameter ','?)+ ;
+parameter : type bindingModifier? IDENTIFIER ;
+
+// Function types have unnamed, space-separated argument types.
+// {int} is the zero-argument type; its function expression still uses ->.
+functionType
+ : '{' type+ '->' type '}' '?'?
+ | '{' type '}' '?'?
+ ;
+
+callSuffix : '(' argumentList? ')' ;
+argumentList : assignment (',' assignment)* ','? ;
+
+// Allocation invokes the declared constructor; inline initializer blocks from
+// earlier chapters are replaced by constructor arguments.
+newExpression : 'new' type ('[' assignment ']' | callSuffix)? ;
+
+INTEGER_LITERAL : '0' | [1-9] DIGIT* ;
+// The f suffix also accepts an integer-shaped token, e.g. 12f.
+FLOAT_LITERAL : DIGIT+ ('.' DIGIT* ('e' DIGIT+)? | 'e' '-'? DIGIT+) 'f'?
+ | DIGIT+ 'f' ;
+STRING_LITERAL : '"' (ESCAPE | ~["\\])* '"' ;
+CHARACTER_LITERAL : '\'' (ESCAPE | ~['\\]) '\'' ;
+fragment ESCAPE : '\\' [0ntr\\'"] ;
+IDENTIFIER : NON_DIGIT (NON_DIGIT | DIGIT)* ;
+fragment NON_DIGIT : [a-zA-Z_] ;
+fragment DIGIT : [0-9] ;
+WHITESPACE : [ \t\r\n]+ -> skip ;
+LINE_COMMENT : '//' ~[\r\n]* -> skip ;
+BLOCK_COMMENT : '/*' .*? '*/' -> skip ;
+```
+
+## Reading the parser alongside the grammar
+
+All paths below are within `src/main/java/com/seaofnodes/simple/parser/`.
+The components share one `Parser` instance containing the cursor, active scope,
+compilation unit, and graph utilities. They are package-private collaborators;
+there is no inheritance chain, AST, or additional compiler pass.
+
+| Productions | Implementation |
+| --- | --- |
+| `program` | [`Parser.parse`](../../src/main/java/com/seaofnodes/simple/parser/Parser.java): establish the source-unit scope, parse its initializer, resolve remaining file names, publish its class |
+| `statement`, `block`, loops, jumps, `returnStatement` | [`Statements`](../../src/main/java/com/seaofnodes/simple/parser/Statements.java), starting at `parseStatement` |
+| `declaration`, `binding`, `bindingModifier` | [`Declarations`](../../src/main/java/com/seaofnodes/simple/parser/Declarations.java): declaration/expression lookahead, then each binding and initializer |
+| `structDeclaration`, `constructorDeclaration` | [`Structs`](../../src/main/java/com/seaofnodes/simple/parser/Structs.java), starting at `parseStruct` and `parseConstructorDeclaration`; field discovery and initialization checks follow these entry points |
+| `type`, `typeName`, `functionType` | [`Types`](../../src/main/java/com/seaofnodes/simple/parser/Types.java): `parseType` and `parseFunctionType`; qualified-name lookup lives in `typeName`, `dottedName`, and `rootedName` |
+| `assignment` through `primaryExpression` | [`Expressions`](../../src/main/java/com/seaofnodes/simple/parser/Expressions.java): `parseAssignment`, `parseExpression`, `parseLogical`, `parseBitwise`, `parseEquality`, `parseComparison`, `parseShift`, `parseAddition`, `parseMultiplication`, `parseUnary`, `parsePrimary` |
+| `postfixExpression`, member selection and updates | `Expressions.parsePostfix` / `parsePostfixMethod` dispatch to [`References.parseField`](../../src/main/java/com/seaofnodes/simple/parser/References.java); `References.parseName` handles bare names, implicit receivers, and forward references |
+| `functionExpression`, `parameterList`, calls, `argumentList` | [`Functions`](../../src/main/java/com/seaofnodes/simple/parser/Functions.java): `parseFunction`, `parseConstructor`, shared `parameters` and `arguments`, then `parseBody` |
+| `newExpression` | [`Allocations.parseNew`](../../src/main/java/com/seaofnodes/simple/parser/Allocations.java); the allocation-helper graph builder follows the expression routines |
+| Literals, identifiers, comments, token matching | [`Lexer`](../../src/main/java/com/seaofnodes/simple/parser/Lexer.java); it also supplies source-location snapshots used by diagnostics and serialized IR |
+
+`ControlFlow` shares guarded scope merging between `if`, `?:`, `&&`, and `||`.
+`Conversions` shares binding, field, and result conversions. These
+operations are compiler semantics rather than additional grammar productions.
+`Parser` retains the type registry and the small control/memory/peephole helpers
+used by several productions and by the rest of the compiler.
+
+There are a few deliberate differences between a syntax-only parser and this
+one-pass IR builder:
+
+- A statement beginning with `{` might be a block or a function-type binding.
+ `Declarations` and `Types` use speculative parsing and cursor restore;
+ qualified type lookup also stops before a suffix that belongs to an expression.
+- Bare assignment, field assignment, and compound updates share the grammar's
+ precedence but use different IR paths. `References` decides between an SSA
+ local, a constructor field's local value, and an actual memory access. A
+ function-valued field may also supply an implicit receiver to a call.
+- `for` updates are scanned first and parsed after the body, where their effects
+ belong. Explicit constructor bodies are scanned first and parsed after field
+ discovery, so later fields have local slots. Both use `Lexer.skipTo`, which
+ ignores delimiters inside comments and quoted text.
+- `Structs.parseInitializer` parses a file or struct body as a function while
+ discovering fields. `Functions.parseBody` still owns function scope/return
+ construction; moving it does not change constructor or memory semantics.
+
+The syntax chart does not describe all name/type restrictions. In particular,
+prefix updates require an already-known non-pointer binding, and a private-class
+privacy policy is separate from recognizing a qualified type name. The existing
+same-named function binding inside a struct is also recognized as a constructor;
+`new Name = { ... };` is the explicit constructor spelling above.
diff --git a/docs/chapter29/README.ja.md b/docs/chapter29/README.ja.md
new file mode 100644
index 00000000..4c697528
--- /dev/null
+++ b/docs/chapter29/README.ja.md
@@ -0,0 +1,39 @@
+# 第29章: 技術的負債の整理
+
+[English](README.md) | 日本語
+
+[前の章: 第28b章 — 分離コンパイル](../chapter28b/README.ja.md)
+
+この章では、コンパイラを読みやすく、変更しやすく整理します。
+最初の変更は、3,000行を超えたパーサを
+文法のまとまりに沿って `parser/` パッケージへ分割することです。
+
+`Parser` がカーソル、スコープ、型の登録情報を持ち、各部品は `Parser P` を
+共有します。構文解析は引き続き Sea-of-Nodes IR を直接生成します。
+`Var` は `parser/` に、反復ピープホール最適化の `IterPeeps` は `codegen/` に移ります。
+
+[パーサの構成(英語)](../../src/main/java/com/seaofnodes/simple/parser/README.md)と
+[文法と実装の対応表(英語)](29-grammar.md)からコードをたどれます。
+インスタンスのフィールドは、初期化中も通常の Load と Store を使います。
+各終了経路のメモリは Return に統合し、最後に全フィールドを再格納する処理は行いません。
+未初期化のフィールドは `TypeScalar.BOT` で表し、meet でも未初期化の分岐が残ります。
+通常のメモリ型の前向き解析で初期化を確認します。詳細は `Construction.java` の
+クラスコメントに記載しています。クラスのシングルトン初期化は従来の方式を維持します。
+ソース上のプライバシー制約は今回は追加せず、[ロードマップ](../roadmap.md)に残します。
+
+[構築グループの実験(英語)](construction-groups.md)では、コンストラクタの
+`C ^arg` 引数で未完成の参照を渡せます。未完成の引数を受け取る new の結果も未完成です。
+`TypeMemPtr` の修飾子を通常の型解析で伝播し、一時的な `ConstructNode` が
+各 new の実引数から結果の修飾子を計算します。
+未完成のオブジェクトのフィールドは self 以外から読めず、通常の関数呼び出しには
+完成した参照が必要です。フィールド要件の推論は後回しにし、対応するテストを保存しています。
+`^` を使うコンストラクタの分離コンパイルは未対応です。
+
+リポジトリのルートからビルドとテストを実行します。
+
+```sh
+make lib tests release CHAPTERS=chapter29 CTAGS=
+```
+
+IntelliJ では `chapter29` モジュールと **chapter29 tests** 実行設定を使用します。
+ネイティブ実行を使うテストの前に、Make でランタイムと `sys` ライブラリをビルドしてください。
diff --git a/docs/chapter29/README.md b/docs/chapter29/README.md
new file mode 100644
index 00000000..b4e8b539
--- /dev/null
+++ b/docs/chapter29/README.md
@@ -0,0 +1,155 @@
+# Chapter 29: Tech Debt Cleanup
+
+English | [日本語](README.ja.md)
+
+[Previous: Chapter 28b — Separate Compilation](../chapter28b/README.md)
+
+This chapter makes the compiler easier to read and change. It starts by
+breaking the parser into grammar-sized pieces. Constructors now check field
+initialization with ordinary memory types, and can pass an incomplete `self` to
+other calls in a type-safe way.
+
+There is lots more cleanup here; earlier chapters are left alone instead of
+backporting the cleanups, in part to show the normal progression of large
+project development.
+
+
+## Following the grammar
+
+The old `Parser.java` had grown beyond 3,000 lines, mixing recursive descent,
+name resolution, scope management, and IR construction. The
+[`parser/` package](../../src/main/java/com/seaofnodes/simple/parser/README.md) separates
+those responsibilities without introducing an AST: parsing still builds
+Sea-of-Nodes IR directly.
+
+`Parser` owns the source cursor, active scope, type registry, and common token
+and IR helpers. Each component shares that state through `Parser P`.
+`Statements`, `Expressions`, `Declarations`, `Types`, `Functions`, and `Structs`
+follow the corresponding grammar productions. `References` handles name and
+field resolution; `Allocations`, `Conversions`, and `ControlFlow` hold shared
+graph-building operations. `Lexer` owns tokenization and source locations, and
+`Var` records lexical bindings.
+
+The [grammar and code map](29-grammar.md) connects productions to methods.
+Start with `Statements.parseStatement` or `Expressions.parseAssignment` and
+follow the calls alongside it. Deferred parsing remains explicit: `for` updates
+are parsed after their bodies, and declared constructors after field discovery.
+
+The [source layout](../../src/main/java/com/seaofnodes/simple/README.md) describes the
+other packages. `IterPeeps` lives in `codegen/` beside the compilation passes.
+
+
+## Checking constructor completion
+
+How constructors are handled has gone back and forth over the chapters; this
+testament to how implementing a language is a journey of exploration more than
+a rote rewriting of the grammer.
+
+Instance fields now use ordinary Loads and Stores throughout initialization.
+Instead of keeping field values in locals until construction ends, the
+constructor works on a real `self` object with private memory. This lets it
+lend `self` to another constructor or function while construction is underway.
+
+Fresh fields start at `TypeScalar.BOT`, which is the uninitialized indicator.
+An initialized integer with an unknown value has `TypeInteger.BOT`, distinct
+from the uninitialized `TypeScalar.BOT`, and similarly for other `BOT` types.
+Stores replace `TypeScalar.BOT` with that field's type in private memory.
+`TypeScalar.BOT` value survives merging at Phis and other control flow, and we
+use it to make sure a field is initialized on all paths.
+
+The completion form of `ConstructNode` checks that every required field is
+initialized on every constructor exit. Once the types prove completion and the
+node adds no pointer restriction, it folds away during optimization.
+Otherwise, `TypeCheck` asks it for an error. This uses the existing type flow
+and error walk; there is no extra construction pass.
+
+Loads check for loading `TypeScalar.BOT` (i.e., loading from uninitialized
+memory), and report an error.
+
+Constructors keep their private-memory and precise aliasing, only losing it
+after the constructor completes.
+
+
+## Incomplete Types
+
+A constructor can pass its partially constructed `self` to other constructors
+and functions. This permits doubly linked lists and parent/child tree nodes to
+cross-link during construction without reading uninitialized fields.
+
+To pass an incomplete type the receiving parameter must declare the partial
+qualifier `^`, for example a`val foo = { Point ^p -> ... }`. The function may
+forward `p`, retain it in another object's private construction memory, and
+read fields proven initialized by every caller. It cannot write through `p`,
+even to non-final fields, or store it into an ordinary object or global. The
+original constructor retains its write permission, including writes to final
+fields until construction finishes.
+
+
+```simple
+struct _Point {
+ int~ x; // final int x
+ int~ y; // final int y
+ new _Point = { int n -> x=n; y=_read(self); };
+};
+val _read = { _Point ^p -> return p.x; };
+return new _Point(arg).y;
+```
+
+Here `x` is readable during `_read`, while `y` is still uninitialized. The
+private names keep the callers within this compilation: unknown callers of a
+public `^` interface promise no initialized fields. If any live caller passes
+a partial without `x` set, the merged parameter loses that guarantee and the
+Load reports an error.
+
+A `new` object is *incomplete* when it starts life and if any of its actual
+arguments are incomplete. With only complete arguments, including null, the
+object is complete after construction. Otherwise it completes with the
+original object; the whole collection completes at once. This allows
+construction to assemble a cycle: after the root construction completes, the
+reachable objects can be used normally. Each constructor must still initialize
+its own fields before returning.
+
+`TypeMemPtr` carries the *partial* qualifier through ordinary type flow. An
+`InitViewNode` combines `self` with its current private memory to describe which
+fields are readable at that point: declared types for initialized fields,
+`TypeScalar.BOT` for the rest. Actual values still come from memory. Calls
+borrowing `self` carry its current Stores on their normal memory edge.
+
+Redundant views fold during optimization. Other views preserve the partial
+restriction or mask unreadable fields until `TypeCheck` checks their consumers
+and removes them. A surviving view is not itself an error: `_read` above needs
+only `x`, not a fully initialized object.
+
+These guarantees remain conservative. Reading a set object-reference field
+does not prove that its referent's fields are initialized. Arrays are already
+initialized and cannot contain partial references. Borrowed reads use aliasable
+memory, so allocating another object of the same class is allowed. Reading an
+initialized final field during construction does not freeze its value: the
+owning constructor may still change it afterward.
+
+Separate compilation of `^` interfaces remains unsupported. Inferred field
+requirements and nested initialization guarantees remain on the roadmap.
+
+The comment in
+[`Construction`](../../src/main/java/com/seaofnodes/simple/parser/Construction.java)
+and the [construction notes](construction-groups.md) explain the type flow,
+memory edges, and remaining restrictions.
+
+
+## Build and test
+
+From the repository root:
+
+```sh
+make lib tests release CHAPTERS=chapter29 CTAGS=
+```
+
+The suite includes the inherited compiler, evaluator, native execution,
+serialization, and separate-compilation tests, plus parser regression tests for
+deferred fragments. Historical test class names retain their original identities.
+The [Chapter 28b library/client example](../chapter28b/README.md) also runs from
+this chapter directory.
+
+IntelliJ has a `chapter29` module and a **chapter29 tests** run configuration.
+Build the native runtime and `sys` library with Make before debugging tests
+that need them; see [the IntelliJ setup](../intellij.md).
diff --git a/docs/chapter29/construction-groups.md b/docs/chapter29/construction-groups.md
new file mode 100644
index 00000000..197a6082
--- /dev/null
+++ b/docs/chapter29/construction-groups.md
@@ -0,0 +1,241 @@
+# Construction: field types and incomplete references
+
+Instance constructors use ordinary field Loads and Stores. There are two checks:
+whether a field has a value, and whether an object reference is usable outside
+construction. Neither requires a collection of constructor-group identities.
+
+## Field initialization
+
+New produces private `TypeMem` containing the object's `TypeStruct`, with
+`TypeScalar.BOT` for fresh fields. Defaults emit ordinary Stores: zero for an
+ordinary integer, null for a nullable pointer, no Store for a required field.
+An assignment strongly updates its field in private memory. Phi meets the
+incoming memories; a missing live arm survives because `BOT.meet(7) == BOT`.
+An initialized unknown integer has `TypeInteger.BOT`, not `TypeScalar.BOT`.
+Arrays retain their language-level zero filling.
+
+One `#selfMem` binding carries this memory through scopes and returns. A Load
+checks its incoming field type. Constructor completion checks the merged Return
+memory; the explicit constructor receives the declaration initializer's merged
+result, including early exits. There is no backwards initialization query,
+per-field parser state, or store-all instance exit.
+
+The existing Iter/Opto passes propagate these types; TypeCheck reports errors.
+There is no extra construction type solve or retained source-use list. An
+initialized field carried unchanged around a loop needs the optimistic SCCP
+solution. Numeric fields in loop memory widen as scalar loop values do.
+Arithmetic, conversions, and numeric Stores preserve Scalar.BOT, so a live bad
+read cannot disappear merely because it is multiplied by zero. Genuinely unused
+reads may be removed by DCE.
+
+The allocator's ConstructNode retains the constructor's final private memory
+for its completion check. TypeCheck gathers these nodes during its existing
+error walk and removes checked boundaries before lowering. The pass removal
+exposes known failures; see [the late-check notes](deferred-construction.md).
+
+Internal construction memory parameters use actual allocation/initializer call
+edges; the hypothetical external caller does not supply their memory. They
+retain their memory family when there are no live callers. Function-valued
+fields retain their declared signatures in private summaries, avoiding repeated
+unfolding of a constructor whose returned memory contains its own function field.
+Class singleton initialization keeps its older store-at-exit implementation.
+
+## Partial references and readable fields
+
+`self` and incoming `C ^arg` parameters are incomplete references. `C? ^arg`
+also accepts null. Ordinary functions and constructors accept the qualifier on
+struct parameters; arrays cannot be qualified this way.
+
+- An allocation result is incomplete if any actual argument is incomplete.
+- With only completed arguments, including null, the result is complete.
+- An incomplete reference may be stored into self or passed to a `^` parameter.
+- Ordinary calls and stores into other objects require completed references.
+- A borrowed partial can read fields its incoming type proves initialized.
+ It cannot write fields, even non-finals. Exact local aliases of self retain
+ the original constructor's access to its current private memory.
+- An object-reference field read through a partial is conservatively
+ incomplete, with no initialized-field guarantees for its referent. There is
+ no inference of initialized-field requirements on `^` arguments.
+ Arrays are already initialized and cannot receive
+ incomplete elements, so an initialized array field can be used normally.
+
+For example:
+
+```simple
+struct Child {
+ Test.Parent? parent;
+ new Child = { Test.Parent? ^par -> parent = par; };
+};
+struct Parent {
+ Child child;
+ int~ payload;
+ new Parent = { int n ->
+ child = new Child(self); // Incomplete result: self is incomplete.
+ // Retention does not authorize reading parent.payload through child.
+ payload = n;
+ };
+};
+val parent = new Parent(arg);
+val local = new Child(parent); // Complete argument, complete result.
+return local.parent==parent; // Lookup is allowed.
+```
+
+Here `Test` names the source unit. After the root Parent constructor completes,
+the entire cycle is usable: all members have initialized their own fields, and
+none could expose an incomplete reference to ordinary code during construction.
+The Child constructor still treats its `^par` as incomplete when the actual
+argument is complete. This public constructor also has unknown callers, whose
+partial arguments promise no initialized fields, so `par.payload` is unreadable.
+
+## Forward field views
+
+At a use of self, `InitViewNode` combines the pointer with that program point's
+private memory. Its TypeMemPtr contains declared types for initialized fields
+and Scalar.BOT for missing fields. The node retains the declaration separately
+from the incoming flow shape, so an exact self alias captured before a Store
+can receive a fresh view afterward. Earlier calls retain their earlier views.
+
+For example, a private helper can read `x` while `y` is still unset:
+
+```simple
+struct _C {
+ int~ x;
+ int~ y;
+ new _C = { int n -> x=n; y=_read(self); };
+};
+val _read = { _C ^p -> return p.x; };
+return new _C(arg).y;
+```
+
+Parm meets the incoming shapes using the normal type flow. If any live caller
+lacks `x`, Scalar.BOT survives and the Load reports the missing initialization.
+Unknown callers supply an empty field promise. Parsing must also start borrowed
+parameters at that bound: otherwise early folding of `p.x*0` could hide a bad
+read before SCCP. The hidden constructor receiver retains its declaration;
+its private memory supplies its read checks and precise field updates.
+
+Values still come from normal memory. Before a call borrowing self, EscapeNode
+borrow slices carry the current constructor Stores onto the ordinary call
+memory edge. These slices are aliasable and do not publish the private identity
+or its fields into the global escape sets. No hidden partial-memory arguments,
+field bitsets, additional passes, or recursive flow queries are introduced.
+
+InitView folds before TypeCheck when its input already has the same type and
+retains a structural partial view. Nested partial-only Construct wrappers also
+collapse. A Parm's current partial type alone is insufficient: inlining may
+later substitute a complete actual argument, which must not grant the borrowed
+parameter permission to write or publish that object. The completion form of
+Construct already folds once its memory proves initialization and its result
+type matches its input.
+
+TypeCheck checks the consumers and erases remaining InitViews along with the
+checked Construct boundaries in its existing walk. A surviving field view is
+not necessarily erroneous: a reader may need only the initialized subset.
+Construct remains necessary for
+final-field completion and for the partial qualifier of each source `new`.
+Alias Phis and nested reference loads are conservative; they do not acquire
+the original constructor's write permission. This prototype adds no general
+ban on overlapping allocations of the same class: only the owning receiver
+uses private memory, while borrowed reads use aliasable memory.
+
+## Aliases and loops
+
+Incomplete is a property of a value, not just the current contents of a parser
+Var. Consider `while (n--) { use(p); p = self; }`: the backedge introduces an
+incomplete value after `use(p)` has been parsed. `TypeMemPtr._partial` carries
+that restriction through ordinary forward typing. It is an independent
+two-point lattice: meet ORs the restriction and dual complements it. Copies,
+conversions, Stores, and Phis preserve it; a merge is incomplete if any live
+input is incomplete. A `^` formal stays incomplete even when its known callers
+are complete. The receiver's private memory identifies the own-self exception;
+another incomplete pointer of the same class has no such permission.
+
+A small `ConstructNode` wraps each source `new`. Its runtime value is the
+allocator result; its type adds the OR of that call's actual argument qualifiers.
+This distinguishes `new Child(self)` from `new Child(null)` even when both call
+the same constructor. Looking only at the shared callee Return would lose this
+distinction. The node disappears after TypeCheck, before lowering.
+
+A completed receiver gives a completed view of its pointer fields. Construction
+may retain incomplete references but cannot publish them, so reaching the root
+`new` boundary with complete arguments makes the entire reachable cycle usable.
+There is no group identity or traversal to mark members complete. Conversely,
+object-reference fields read through incomplete self remain conservative, even
+if the particular stored reference was complete.
+
+The existing optimistic SCCP solve also handles qualifier cycles. For
+`p = null; while (n--) p = new Link(p);`, no input introduces incompleteness,
+so `p` stays complete and a subsequent field read is allowed. A backedge carrying
+self instead makes the Phi and its uses incomplete.
+
+`Construction` selects private self memory during parsing; it retains no
+source-use lists or flow facts and performs no recursive Load/Phi/Call query. The old
+root and allocation bitsets are gone. There are no field-requirement sets,
+hidden `^` memory arguments, or runtime group objects.
+
+`PartialReadTest` exercises field reads, missing fields, forwarding, merged
+callers, loops, exact aliases, constructor retention, and borrowed write and
+publication restrictions. `ConstructionGroupTest` exercises cycles, complete/null arguments, prohibited
+lookups/publication, aliases, branch merges, complete and incomplete loop
+backedges, and declared restrictions with only complete callers, including an
+uninlined constructor through machine encoding. `ConstructorCompletionTest`,
+`FinalConstructionTest`, and `BoundaryTest` exercise field-memory semantics.
+The former inferred-requirement litmus tests are preserved, explicitly deferred,
+in `ConstructionRequirementsTest`. Separate compilation of `^` functions is
+still deliberately unsupported. The pointer qualifier has an object-file
+representation, but importing and checking the full construction contract is
+outside this experiment. Rebuild chapter 29 object files after the type-format
+change.
+
+## Deferred field-aware contracts (2026-11-07)
+
+Decision: do not implement inferred contracts now. Cliff considers the required
+infrastructure too large for the feature alone; revisit when a general
+backwards/liveness flow is justified, potentially for memory management.
+The later forward-field-view prototype above does not implement this contract
+discovery proposal. Separate compilation of `^` interfaces remains rejected.
+
+The intended extension would let functions and constructors receive partially
+initialized objects, read initialized fields, retain partial references with
+their restrictions, and write non-final fields. Only the original constructor
+would retain permission to initialize/reassign its own final fields; passing a
+pointer must not pass that permission. Final-field reads during construction
+would not establish value stability against later writes by that constructor.
+
+The proposed contract representation uses normal types, not a separate field
+bitset. For a function reading only `P.pi`, its unknown-caller default would be
+schematically `Partial{pi:declared-pi-type, name:TypeScalar.BOT}`. Scalar.BOT
+means no initialization guarantee, not a requirement that the caller leave the
+field unset. Required fields receive their declared types, covering every legal
+external caller. MEET with actual arguments preserves those guarantees only
+when the callers satisfy them; extra initialized fields supplied by locals do
+not strengthen the body beyond the external contract. TypeStruct/Field already
+meet field contents, and nested/cyclic types can represent nested contracts.
+TOP is not the missing-initialization marker.
+
+Cliff proposed discovering conservative, context-insensitive requirements after
+Iter and before SCCP. Freeze the resulting default argument types on functions;
+SCCP consumes them without refining the contracts. Reads on branches SCCP later
+eliminates may still impose requirements, as may reads following assignments.
+That rejection of some valid programs is acceptable for an opt-in `^` feature.
+Combine compatible requirement shapes with JOIN; combine possible caller states
+with MEET. Keep the contract distinct from the merged Parm type, and check each
+actual argument against it for a call-site diagnostic such as
+"foo requires Partial.pi to be initialized". Serialize contracts with declaration
+metadata so checking imported calls does not require loading bodies.
+
+The complication is discovering and propagating those contracts. Local SSA
+aliases share values; Phi demands reach every input. A demand on a loaded field
+becomes a nested receiver requirement. Calls substitute actual arguments for
+formals, and recursive forwarding needs a convergent propagation rule. Cyclic
+types represent recursive requirements but do not by themselves prevent a
+discovery algorithm from unfolding progressively deeper paths. Indirect calls
+need contracts available through their callable interfaces; returns need usable
+guarantees without retroactively strengthening an exported contract for a client.
+Stored references and helper writes also require a coherent account of private
+construction memory and object identity; that discussion was deliberately left
+unfinished. None of this requires H-M or bidirectional type inference, but it
+does require a substantial backwards demand analysis to handle well.
+
+The ignored ConstructionRequirementsTest retains useful litmus cases, not an
+approved implementation plan. Do not restart this work without a new request.
diff --git a/docs/chapter29/deferred-construction.md b/docs/chapter29/deferred-construction.md
new file mode 100644
index 00000000..104ab9e9
--- /dev/null
+++ b/docs/chapter29/deferred-construction.md
@@ -0,0 +1,385 @@
+# Deferring construction checks: experiment
+
+The [escape-flow continuation](escape-flow.md) supersedes the frozen-public
+fallback and remaining recursive-test failures described later in these notes.
+All four recursive examples now pass their original printed-IR expectations.
+The older audit below records the evidence that led to this change.
+
+The extra construction pass is removed from the working implementation.
+`Construction.check()`, its retained source-use lists, `Iter.types()`, and the
+separate constructor-Return walk are gone. Errors now belong to the existing
+TypeCheck walk; ConstructNode retains final private memory for completion checks.
+No additional whole-graph pass was added. Constructor rejection tests remain active.
+
+## Constant folding
+
+`TypeScalar.BOT` is not an initialized integer. Arithmetic, comparisons,
+negation, numeric conversions, and typed Stores must preserve it rather than
+manufacturing an integer or floating-point result. In particular, `x*0` and
+`x==x` cannot become constants while `x` has this type. An initialized unknown
+integer still permits those folds.
+
+The scan found and changed these related cases:
+
+- Multiplication by zero now folds in `compute`, after checking numeric inputs.
+ Integer strength reduction also waits for integer operands; an unfinished
+ loop Phi could eventually become a float.
+- Same-input comparisons fold in `compute`. Floating comparisons keep NaN
+ semantics; `x==x` is not necessarily true. The integer range implementation
+ of `!=` also had the equality answers reversed.
+- Arithmetic pushed through Phis uses each copied operation's `compute`,
+ including floating-point constants, instead of an unchecked integer cast.
+- Known array length offsets and element shifts fold in
+ `ConFldOffNode.compute`, rather than its `idealize`.
+- `Not`, unary minus, conversions, and Stores preserve unknown initialization.
+ `RoundF32` already passed that type through.
+
+This does not ban creating constant *operands* in rewrites such as `x*8` to
+`x<<3`. Memory forwarding still has cases that return a stored constant or an
+array's zero-filled element; these were inspected but not rewritten into a new
+memory analysis. `Load.idealize` retains a `Scalar.BOT` load.
+
+The changes exposed a rejected-Phi-fold worklist cycle. Factoring
+`Phi(bool & 7, integer & bool)` widens the result from `bool` to `[0-7]` and
+must fail. Building and then killing the candidate woke the same dependencies
+forever. The trial now computes its types before registering use edges.
+
+## Preserving the constructor body through optimization
+
+This regression must reject a live incomplete lookup:
+
+```simple
+struct Child {
+ Test.Parent? parent;
+ new Child = { Test.Parent? ^par -> parent = par; };
+};
+struct Parent {
+ Child child;
+ int~ payload;
+ new Parent = { ->
+ child = new Child(self);
+ payload = child.parent == null;
+ };
+};
+return new Parent().payload;
+```
+
+`child` is incomplete because its constructor received incomplete `self`.
+Reading `child.parent` must therefore fail. The read supplies the final result;
+this is not a discarded local or dead-code issue.
+
+The decisive failure was private-memory Parm typing. During Iter, inlining
+removed a real caller while the function still had its unknown-caller edge.
+The special case returned MemTop anyway. That erased the memory merge, Store,
+and the bad Load feeding it, before TypeCheck could report the error.
+
+Parms now use their normal merge. Hidden initializer/constructor helpers have
+only actual allocator callers; the source-visible constructor binding denotes
+the allocator. Public allocators still receive unknown callers, so unused public
+constructor bodies remain checked. This also avoids poisoning a helper's private
+memory with a fictitious external default.
+
+Inlining presents a second issue: a complete actual pointer must not grant
+permission to read a restricted formal. A partial-only ConstructNode gives
+self and ^ parameters their body view independently of Parm typing. It remains
+until TypeCheck. Load's alias search recognizes that this view preserves the
+address, while compute/err continue to see the qualified pointer.
+
+A smaller regression reads an unwritten field through a self cycle:
+
+```simple
+struct C {
+ C next = self;
+ int~ x;
+ new C = { -> x = next.x; };
+};
+return new C().x;
+```
+
+Both examples now reject at TypeCheck. A positive regression initializes x to
+7, increments it in the constructor, and checks that the result folds to 8.
+
+The broader run exposed a function-lifetime bug during SCCP: unlinking calls
+could delete a body subsequently rediscovered from a type-level function
+reference. Returns are kept during the existing reset walk and released once
+call edges have been rebuilt. No additional graph walk is needed.
+
+Class-init dependency discovery also skips a CLInit whose receiver has not
+resolved to a class. A Scalar.BOT receiver is left for its Load's TypeCheck
+error, rather than dereferencing a null class name.
+
+The trial successfully uses the existing passes to prove a chain built from
+`null` complete. It also rejects direct `par.payload`, `par.payload*0`, and
+`int x = par.payload; value = x*0` after the arithmetic/conversion repairs.
+
+Genuinely unused reads are allowed to disappear under late-check semantics.
+Rejection tests must keep the read live. `testUnusedIncompleteReadCanDie` checks
+that an unused `par.payload` read is eliminated and the program returns 5.
+
+## Optimization differences and register allocation
+
+`Chapter18Test.testFcn9` exposed an independent escape bug. StopCU treated the
+class initializer's synthetic self as an object already held by an external
+caller. Its broad function-valued field type exposed the local x function even
+though the initializer never returned and x had no callers after inlining.
+Skipping that synthetic argument restores the empty escape set for x and the
+expected `return Top;`. Actual public-call arguments still escape normally.
+
+That correction exposed `Chapter18Test.testFcn8`: a Store computed from TOP
+memory interpreted its optimistic singleton bit as established private memory.
+It widened a function value to the declared signature, then narrowed its FIDX
+set when memory resolved. Store now returns TOP while its memory is TOP, so
+SCCP remains monotone.
+
+The String differences have a different cause. In Chapter19, the private
+_hashCodeString body dies; the surviving functions are the initializer,
+allocator, equals, and public hashCode. Its golden string now omits the two dead
+private-helper MemTop Returns and matches the current addition order.
+`Add.spine_cmp` breaks this tie by node ID. Strength reduction creates the Sub
+in IterPostOpto, after the byte Load (Sub#594 versus Load#504 in one traced copy),
+so the Sub sorts to the left. This is node ordering, not a lost value type.
+
+In the Chapter20/21 fixtures, _hashCodeString really escapes through the implicit
+return, and hashCode contains an inlined copy. Before the allocator repair, both loops needed an extra x86
+register copy with the new addition operand order. A disposable probe
+swapped only `((hash<<5)-hash) + byte` to `byte + ((hash<<5)-hash)` after Unlink:
+no types or live functions changed, but the weighted split score fell from 37
+to 21. Each hash body fell from 17 to 9; the allocator stayed at 3.
+
+Reconstructing the earlier source by reversing the saved construction-pass
+removal patch reproduces the expected 29. Its standalone _hashCodeString costs
+17, inlined hashCode costs 9, and allocator costs 3. Before the allocator repair,
+these costs were 17, 17, and 3. Both versions contain two hash loops; no inlining decision changed.
+Current code emits two fewer functions, the dead initializer/constructor helpers,
+which contributed zero. Thus the entire increase is one loop-weighted register
+copy in the inlined hashCode body, not another retained function or stack spill.
+
+With the old operand order, the byte Load can write the loop's hash register
+and Add accumulates the shifted/subtracted value into it. With the new order,
+Add's destructive input is the Sub result. Its two-address tie plus the Phi
+backedge joins the shift/Sub/Add chain to the old hash live range, which overlaps
+the shifted value at the subtraction. `RegAlloc.splitSelfConflict` inserts a
+`use/self/two` copy before Add. The existing commute heuristic in `splitByLoop`
+is not reached: self conflicts take an earlier splitting path. Commuting Add
+before splitting is the concrete optimization opportunity.
+
+The allocator now does this in `splitSelfConflict`, using the existing failed-
+range traversal and scratch list. It commutes two-address operators when the
+RHS has one use and belongs to a different live range, then omits the copy for
+the tie it broke. An allocator-generated Split is excluded: its single use can
+hide a longer-lived value, and commuting onto it can undo an earlier repair.
+The existing coloring retry rebuilds live ranges. There is no new whole-IR pass
+or analysis structure. The machine's `commutes()` hook covers Add, Mul, Xor,
+And, and Or (and commutative floating instructions).
+
+The String score is now 21: each hash body costs 9 and the allocator costs 3.
+The three x86 expectations are updated to 21. A single regression exercises
+all five integer operators in both machine operand orders, checks register
+constraints, and encodes the results. Existing testFlags2 guards against the
+Split-operand regression.
+
+## Incremental Iter setup
+
+Iter no longer resets and requeues the whole graph after parsing. `Node.init()`
+queues initialized nodes, and Calls queue themselves so Iter can link targets
+loaded after the call was parsed. Iter seeds its new global escape summary,
+then drains the worklist. Import loading still resets the newly loaded nodes
+and repairs affected neighbors.
+
+Removing the sweep also exposed an unused receiver left behind when field
+lookup switches from an instance to its class. That path now releases the keep
+and kills the receiver if unused.
+
+FldOffNode's base is data in slot 1. Slot 0 is reserved for control; putting a
+data parameter there made inliner body discovery exclude an unresolved offset.
+The regression `CallEndTest.testUnresolvedOffsetInInlineBody` checks that the
+offset belongs to the function body even before its type resolves.
+
+## Type upgrades in Opto setup
+
+ParseAll still closes the named types and freezes the public interface after
+loading all units. It no longer walks the IR. Opto installs those closed types
+and resolves forward references during its existing reset walk. Setup runs no
+peepholes against the partly reset graph: resolved FRefs propagate their input
+type in SCCP and disappear during subsequent peepholes. Construction identities
+also wait until this setup has installed complete field declarations.
+
+That walk retains Returns while call edges are rebuilt, gathers unresolved
+numeric modes, and records dependencies for cleanup after SCCP. Type upgrades
+also register cleanup dependencies, even if the eventual value type is unchanged.
+Old Iter types are snapshots for change detection, not upper bounds after
+recursive declarations have been closed. SCCP still asserts monotone falling
+types and a complete worklist within its own solve.
+
+The move exposed assumptions formerly hidden by early upgrades. Resolving an
+already known class name must retain its known fields instead of replacing them
+with an empty placeholder. A Load must not forward a stored pointer whose open
+shape is weaker than the Load's computed type. Escape must retain escapes from
+the actual private value before narrowing it to its declared field type, so
+selecting a Store out of a MemMerge does not introduce new escapes.
+
+An unknown return or memory effect carries FULL escape sets. StopCU and
+GlobalEscape preserve that bound: narrowing it to public names would omit
+private objects and callbacks that a subsequent load-forwarding rewrite can
+reveal. Both summaries recompute from their inputs without retaining previous
+escape sets. Refining a value removes possibilities; it never adds escapes
+outside the old bound. IterPeeps has no special summary handling, and its
+worklist assertion requires every node's type to lift, including queued nodes.
+`testUnknownEscapesRefineToPrivateValues` checks both escape sinks;
+`testPrivateRecursiveReturn` covers cyclic private objects and field loads.
+
+The ordinary dependency mechanism also covers signature refinement and lazy
+imports. FunNode depends on its Return when refining its signature. Calls
+depend on their target FunNode; while a target is absent, they depend on Start
+and retry when a linker mapping is installed. Loading bodies and adding call
+edges still use `Iter.repair` for graph changes, not for value refinement.
+
+SCCP's Start linking honors FULL for private targets too. Incoming arguments
+are not themselves outgoing escapes; Stores, Returns, and external-call
+arguments describe those effects. Constant singleton field slices carry the
+stored value's escapes, not unrelated bulk-memory escapes frozen at the time
+of folding. This preserves precision for local recursive callbacks.
+
+As bounds sharpen after SCCP, FunNode removes unknown callers even if concrete
+callers remain. Called initializers retain their Return through StopCU because
+CLInit has no Return edge. Removing an uncalled function queues the affected
+Stops, and compilation-unit wrappers retain distinct identities even when
+empty. These are worklist notifications and ownership rules, not extra passes.
+
+### Remaining precision issue with FULL escapes
+
+The monotone bounds expose a feedback problem in the existing grand cycle.
+In `Chapter12Test.testPrivateRecursiveReturn`, SCCP sees BOTTOM for the final
+load chain and therefore FULL escapes. These feed through Start's memory into
+the function memory results. Post-SCCP forwarding resolves the return to `_B`,
+but that memory cycle remains at MemBot and keeps private constructors callable
+by unknown callers. This is conservative, but loses the previous dead-function
+elimination. It does not justify narrowing BOTTOM to public names or adding
+the IterPeeps widening repair back.
+
+For this example, the root Return goes from `[Ctrl, MemBot, Bot]` after Opto to
+`[Ctrl, MemBot, *_B]` after TypeCheck; Stop's memory remains MemBot. The remaining
+design question is how to avoid this feedback while retaining conservative
+bounds and the existing pass budget. The current experiment leaves the printed
+IR expectations unchanged so they continue to expose this loss of precision.
+
+The source-validity audit separates these cases from Chapter10 `testIfOrNull`.
+That test originally returned the nullable pointer itself. A historical edit
+changed it to `return bar.a;` without adding TypeCheck, leaving a real null
+error hidden behind a printed-IR assertion. It now again returns `bar`, runs
+through TypeCheck, and passes with `return Phi(Region,null,Test._Bar);`.
+
+All four remaining programs pass TypeCheck. The Chapter12 recursive-object
+tests have one pending bad Load after Opto; forwarding eliminates every Load
+and resolves the result to `17`, `_B`, or `_R`, while escape memory stays MemBot.
+
+The two Chapter17b forward-type tests now pass their original IR expectations
+and explicitly run TypeCheck. Their undeclared `B` is an opaque external type,
+not an arbitrary unknown local value. EscapeNode uses the frozen public
+interface as its fallback for that type, preserving escapes already present
+in its private and public memory inputs. This applies to both the actual private
+value and the declared field bound: in `testForward1`, the private array element
+type is already `B?`. Bounding only FIDXs is insufficient; FULL aliases expose
+private class fields, which then expose their constructor FIDXs.
+
+CodeGen caches the fallback when ParseAll freezes the public interface. It
+returns FULL until Opto's existing reset walk installs the closed types into
+the IR. Only then does an open `_fref` reliably denote an external object.
+Ordinary open local structs, `TypeScalar.BOT`, and abstract pointers still
+require FULL. No new pass or IterPeeps hook is involved.
+
+### Escape-bound audit
+
+All Chapter29 `escapesFrom`, `XInt.FULL`, and `TypeMem.BOT` uses were reviewed,
+including the equivalent `escapes` helper and indirect `makePrivate` uses.
+The following remaining sites warrant work; they have not been narrowed here:
+
+* **StoreNode's three `escapesFrom(val)` paths.** The singleton constant,
+ private instance, and ordinary aliasing paths still treat an opaque external
+ value as FULL. A concrete example is:
+
+ ```simple
+ struct _A { B? !b; new _A = { B? p -> b=p; }; };
+ val wrap = { B? b -> return new _A(b); };
+ return wrap;
+ ```
+
+ This passes TypeCheck, but the surviving Stores of `b` have FULL FIDX and
+ alias sets and keep an extra `_A` allocator. Do not mechanically substitute
+ the public default: singleton slices deliberately discard unrelated incoming
+ memory escapes. A narrower opaque bound must retain private values previously
+ given to external code, without freezing unrelated transient escapes into
+ singleton constants. Ordinary aliasing Stores already retain incoming sets.
+
+* **StopCU return values, CallNode external arguments, and SummaryNode arguments.**
+ These use `escapes`, which currently duplicates the unbounded `escapesFrom`.
+ `val identity = { B? b -> return b; }; return identity;` passes TypeCheck but
+ gives StopCU MemBot. Likewise,
+ `val send = { B? b -> { B? -> int } retain = "C"; return retain(b); }; return send;`
+ gives the native CallEnd MemBot. These are concrete instances of the same
+ opaque-type precision loss. A future change can share the bounded helper,
+ while retaining incoming/returned memory, explicit private arguments, and
+ imported-summary effects. StopCU's public function seed and `escapesAliases`
+ class seed describe known declarations; GlobalEscape's `escapes` calls merely
+ union already computed TypeMem sets.
+
+* **CLInitNode's possible-initialization result.** Even a known initializer
+ yields TypeMem.BOT until its ready flag is true; only the initialized flag's
+ slice is precise. This loses both memory-content and escape precision.
+ CLInit has an analysis call edge but no Return edge supplying callee effects.
+ Improving it needs a dependency on the initializer's effects, including
+ recursive dependencies. The public interface alone is not a safe cap: that
+ initializer can publish private callbacks. Its unresolved-flag BOT fallback
+ is also conservative, but normally disappears when the receiver resolves.
+
+* **CallEndNode's unknown or unlinked target result.** Both return TypeMem.BOT.
+ The unlinked case covers invalid calls and transient missing edges; SCCP links
+ valid finite targets before computing CallEnd. Truly unknown targets can be
+ local private functions as well as external functions, so a public-only cap
+ would be wrong. Native calls already use CallNode's argument/memory effects.
+ Any refinement must distinguish target provenance and retain explicit private
+ arguments; it is not the same small fix as an opaque external object.
+
+The other broad defaults appear justified:
+
+* TypeMem's generic BOTTOM/scalar/pointer cases and TypeStruct's unknown scalar
+ fields must include private callbacks that later value refinement can reveal.
+ Private-memory declarations/Phis use `makePrivate(BOTTOM)` for the same reason.
+ MemMerge's `makePrivate(struct)` describes actual fields, not an external
+ opaque object.
+* Parser/function memory declarations, initial node types, TypeTuple.STATE,
+ and low-family error fallbacks in Store, MemMerge, BulkMemPhi, Start, and
+ TypeTuple need worst-case memory. CallNode and SummaryNode likewise cannot
+ promise bounded effects when their inputs are still arbitrary low values.
+* Iter's import reset gives newly loaded nonconstant memory FULL before repairing
+ affected neighbors. Saved types belong to another caller context; constants
+ retain their own computed types. This is a safe pessimistic seed, though it
+ can contribute to the existing memory-cycle precision problem.
+* TypeFunPtr's FULL signatures and TypeRPC.BOT describe unknown function/return
+ targets. Serialization/remapping preserves the FULL sentinel; type gathering
+ exercises it; LoopNode's `isa(TypeMem.BOT)` is a type-family check. These do
+ not independently manufacture an escape effect.
+
+Ignoring assertions, the remaining whole-node walks through TypeCheck are
+Opto's setup/reset and TypeCheck's error scan. Class-init checking separately
+scans function control flow and runs SCC over the resulting dependency graph.
+
+## Validation
+
+Chapter29's latest run tested 594 active cases with assertions: 590 passed and
+four printed-IR comparisons failed. `make -k tests` ran raw0 (400/404), raw1
+(38), standalone (13), system (29), and fuzzer (1); the aggregate group (109)
+was run separately because Make skips it after a prerequisite failure.
+The remaining failures retain extra private constructors: Chapter12 `testCoRecur`,
+`testCoRecur2`, `testCoRecur3`, and `testPrivateRecursiveReturn`. Both Chapter17b
+forward tests pass unchanged goldens. TypeTest.testOpaqueExternalEscapes checks
+both fallback sets, preservation of prior private escapes, and FULL for ordinary
+unknowns. System, spill, import, callback, and constructor tests pass. The only
+golden changed during the earlier source-validity audit belongs to the restored
+`return bar;` source; none were changed to accept extra functions.
+
+The offset regression fails with the old slot-0 layout. Constructor and
+boundary tests pass, including both missed rejections and the prior NPE.
+Printed-IR expectations omit the dead private-helper MemTop Returns; no
+rejection expectations were relaxed. The three String spill checks now expect
+21 on x86, with the same live functions and inlining decisions.
diff --git a/docs/chapter29/escape-flow.md b/docs/chapter29/escape-flow.md
new file mode 100644
index 00000000..8ad49f15
--- /dev/null
+++ b/docs/chapter29/escape-flow.md
@@ -0,0 +1,147 @@
+# Outside-world escape flow
+
+The `escape-flow` branch developed this design in Chapter29 and backported it
+to Chapters 28a/28b. Chapter28a uses `LocalIds` and its existing initializer
+root/lowering protocol; Chapter28b adds saved summaries and cofinite identity
+remapping. Neither port imports Chapter29's deferred-construction model.
+
+## The cycle
+
+The outside world's access to local objects and functions is a value in the
+Start/Stop cycle. Public declarations are not that value. The frozen public
+interface inventory has been removed; public-method and public-alias tests
+check the converged outside escape sets directly.
+
+At Opto setup, the outer Start receives a constant foreign-identity seed:
+
+```
+foreign identities = FULL minus identities owned by this compilation
+outside identities = foreign identities union discovered local escapes
+```
+
+Known native function and storage identities are foreign too. The infinite
+tail permits future foreign functions even when no local callback has escaped.
+The finite exclusions include registered imported identities, including bodies
+represented by summaries. This scans identity tables, not the IR. Opto's
+existing reset/worklist solve discovers the local escapes.
+
+StartCU includes the shared outside state. Infinite FIDX sets must retain their
+exclusions: `Opto.linkStart` checks each local function's membership rather than
+treating every cofinite set as FULL. Serialization remaps a cofinite set by
+remapping its finite exclusions and complementing again.
+
+Before Opto, outside queries remain pessimistic. During SCCP, a consumer can
+read the foreign seed before Start has run: otherwise a temporary finite set
+can turn a generic foreign callback into an unrelated local function with the
+same arity. No previous escape result is accumulated to repair value flow.
+
+## Consumers and dependencies
+
+* Memory and callback defaults for unknown callers use Start's memory
+ projection. Parm applies its declared callback signature to the escaping
+ FIDX set. Defaults whose values do not depend on escapes remain constants.
+* Unknown/native CallEnd results use the outside state and the current call's
+ exposed arguments. Callback results are bounded by their declared signature
+ and the outside FIDX set. Explicit linked Return effects are retained.
+* CallEnd registers a direct dependency on Start. Depending only on Call is
+ insufficient: Call can stay CONTROL while the outside set changes. This
+ first implementation uses `addDepForwards`; it does not change CallEnd's
+ input layout or introduce a synthetic Fun/Return pair.
+* Infinite calls use the outside continuation. Escaped local functions remain
+ callable through Start, and their effects return through Stop. Attaching
+ every same-arity body to an infinite call is incorrect: argument/result
+ families may differ despite the equal arity.
+* Outgoing Call argument exposure is independent of CallEnd, including a call
+ that never returns. The shared fixed point also permits immediate callback
+ round trips and reentrant calls.
+* Opaque external values in Escape, Store, Return, and imported-summary paths
+ use the current outside/input-memory bound. Explicit private values and
+ already accumulated escapes remain included. Arbitrary local BOTTOM values
+ still require FULL.
+* Function-valued Loads use their memory input's FIDX bound, joined with the
+ field declaration. They do not independently manufacture an escape set.
+
+CLInit was another unconditional producer of FULL. During Opto it now depends
+on the known initializer's Return and merges those effects with incoming
+memory. It waits for live memory before publishing effects. Lowering computes
+the replacement CallEnd's linked type before exposing its projections, so a
+temporary unlinked MemBot cannot widen the already analyzed caller.
+
+## Why the recursive tests needed local precision too
+
+All four former Chapter12 printed-IR failures pass their original expectations.
+The problem was not solely an outside-call default:
+
+1. Convert widened a non-null pointer to a nullable declaration until its later
+ identity rewrite. It now preserves non-nullness in compute. Preserving the
+ entire source shape conditionally failed the existing monotonicity test;
+ shape and access still follow the declaration.
+2. A Load could wait until post-SCCP Store forwarding to recover that non-null
+ pointer. In the meantime the next dereference produced BOTTOM, which fed
+ FULL around the memory cycle and revived private constructors.
+3. Forwarded addresses, resolved field aliases, and already-bypassed CLInit
+ projections were also waiting for graph rewrites.
+
+Load's existing forwarding search now supplies pointer value facts during
+compute, without constructing or rewriting nodes. It can recognize a stored
+address through a valid pointer conversion or a proven forwarded Load, use a
+resolved named field before layout, and follow an existing CLInit bypass.
+The same dependency mechanism reruns the search when its evidence changes.
+Nullable/incomplete access checks still precede this sharpening; arbitrary
+memory Phis and unknown stores remain barriers. This adds local queries, not a
+new whole-program pass, analysis map, or second IR.
+
+## Hidden class receivers
+
+A class function's hidden `#self` defaults to its owning class singleton.
+Seeding that input with TypePtr.PTR used to give FULL escapes when forwarded
+to an unknown callback. For example:
+
+```simple
+val _hidden_noInline = { int n -> return n+2; };
+val apply_noInline = { {int->int} f,int n -> return f(n); };
+return apply_noInline;
+```
+
+`FunNode.defaultArg` now supplies the Test class singleton for this receiver,
+both during parsing and when Opto reconnects unknown callers. The callback
+parameter still reads Start dynamically. The printed-IR regression verifies
+that only `<clinit>` and `apply_noInline` escape; `_hidden_noInline` stays private.
+
+The function signature retains TypePtr.PTR in this slot. The backends use it
+to identify the hidden ABI argument, and callback annotations must not bind
+the receiver to the class where the annotation appears. Explicit calls still
+supply their actual receiver; instance methods keep their declared object
+type. This sharpens the default input, not every abstract pointer: a local
+merge of a private object and private callback can also have an abstract
+pointer type, so capping arbitrary TypePtr values at outside escapes is unsound.
+
+## Remaining design question
+
+Late imported identities also deserve further precision work: identities not
+registered when Opto starts lie in the foreign tail. Existing import tests
+exercise correctness, but this can remain conservative when bodies arrive
+later. This limitation is not repaired by resetting the whole IR.
+
+## Validation
+
+The combined `make -k tests CHAPTERS="chapter28a chapter28b chapter29" CTAGS=`
+run passes with assertions: 538 active tests in Chapter28a, 578 in Chapter28b,
+and 601 in Chapter29. Shared printer and ISA checks pass too. The log is saved
+under ignored `build/escape-backport-final.log`.
+
+`EscapeFlowTest` covers opaque wrappers with three worklist seeds, foreign
+callback parameters, private callbacks passed to and returned from native code,
+and a CallEnd dependency that responds when only Start's escape set changes.
+It also checks that public allocators escape while their private-memory helpers
+do not. Class-init tests cover unknown callback cycles; saved-library tests in
+28b/29 verify private-function exclusions survive identity remapping.
+The existing suites cover nonreturning native calls, class-init cycles,
+constructor rejection, serialization, imported callbacks, and three backends.
+The four recursive-class examples also pass a separate 16-case worklist-seed
+check. Backport IR expectations now retain public allocator Returns instead of
+private-memory helper Returns. The String allocation/encoding/export fixtures
+in 28a/28b/29 now declare `_hashCodeString` before the public `hashCode` wrapper,
+so the implicit return exposes the wrapper. Their measured spill expectations
+are 12/8/6 (x86/RISC-V/ARM); native x86 export expects 12. The reordered fixture
+also measures 8 on RISC-V before the backport, versus the old order's 6.
diff --git a/src/main/java/com/seaofnodes/simple/Parser.java b/src/main/java/com/seaofnodes/simple/Parser.java
deleted file mode 100644
index cef7091a..00000000
--- a/src/main/java/com/seaofnodes/simple/Parser.java
+++ /dev/null
@@ -1,3200 +0,0 @@
-package com.seaofnodes.simple;
-
-import com.seaofnodes.simple.codegen.*;
-import com.seaofnodes.simple.node.*;
-import com.seaofnodes.simple.type.*;
-import com.seaofnodes.simple.util.Ary;
-import com.seaofnodes.simple.util.BAOS;
-import com.seaofnodes.simple.node.ScopeNode.Kind;
-
-import static com.seaofnodes.simple.util.Utils.TODO;
-
-import java.util.*;
-
-/**
- * The Parser converts a Simple source program to the Sea of Nodes intermediate
- * representation directly in one pass. There is no intermediate Abstract
- * Syntax Tree structure.
- * <p>
- * This is a simple recursive descent parser. All lexical analysis is done here as well.
- */
-public class Parser {
-
- // Compile driver
- public final CodeGen _code;
-
- // The Lexer. Thin wrapper over a byte[] buffer with a cursor.
- private Lexer _lexer;
-
- // Class prefix string; TODO: use something short: "%"
- public static final String CLZ = "class:";
- private static final String clzPrefix = CLZ;
- public static String addClzPrefix( String x ) {
- assert !x.startsWith(clzPrefix);
- return (clzPrefix+x).intern();
- }
- public static boolean startsClzPrefix( String s ) {
- return s.startsWith(clzPrefix);
- }
- public static boolean isPublicClassName( String name ) {
- if( startsClzPrefix(name) ) name = name.substring(clzPrefix.length());
- return !name.isEmpty() && name.charAt(0)!='_' && name.indexOf("._")==-1;
- }
-
- // Source file for compilation
- public CompUnit _ref;
-
- // Current classname being parsed
- private String _nestedType;
- /**
- * Current ScopeNode - ScopeNodes change as we parse code, but at any point of time
- * there is one current ScopeNode. The reason the current ScopeNode can change is to do with how
- * we handle branching. See {@link #parseIf()}.
- * <p>
- * Each ScopeNode contains a stack of lexical scopes, each scope is a symbol table that binds
- * variable names to Nodes. The top of this stack represents current scope.
- * <p>
- * We keep a list of all ScopeNodes so that we can show them in graphs.
- * @see #parseIf()
- * @see #_xScopes
- */
- public ScopeNode _scope;
-
- /**
- * List of keywords disallowed as identifiers
- */
- public static final HashSet<String> KEYWORDS = new HashSet<>(){{
- add("bool");
- add("break");
- add("byte");
- add("continue");
- add("else");
- add("f32");
- add("f64");
- add("false");
- add("flt");
- add("i16");
- add("i32");
- add("i64");
- add("i8");
- add("if");
- add("int");
- add("new");
- add("null");
- add("return");
- add("struct");
- add("true");
- add("u1");
- add("u16");
- add("u32");
- add("u8");
- add("while");
- }};
-
- /**
- * We clone ScopeNodes when control flows branch; it is useful to have
- * a list of all active ScopeNodes for purposes of visualization of the SoN graph
- */
- public final Stack<ScopeNode> _xScopes = new Stack<>();
-
- ScopeNode _breakScope; // Merge all the while-breaks here
- ScopeNode _continueScope; // Merge all the while-continues here
- ScopeNode _returnScope; // Merge all the function exits here
- Ary<FRefNode> _frefs; // Forward refs created during this parse
- // Layouts needed by field accesses in this source file. Resolve after
- // parsing, since a later local struct declaration can satisfy the use.
- public final LinkedHashSet<String> _requiredTypes = new LinkedHashSet<>();
- TypeStruct _ctorOpenStruct; // Open struct being initialized by an allocation constructor block.
- // Constructor exit types survive scope teardown until all fields are known.
- // A null entry denotes a constructor with no returning path.
- private ArrayList<IdentityHashMap<Var,Type>> _ctorExits;
-
- // Mapping from a type name to a Type. The string name matches
- // `type.str()` call.
- public static final HashMap<String, Type> TYPES = new HashMap<>();
- public static final HashMap<String, Type> INIT_TYPES =
- new HashMap<>() {{
- put("bool",TypeInteger.U1 );
- put("byte",TypeInteger.U8 );
- put("f32" ,TypeFloat .F32);
- put("f64" ,TypeFloat .F64);
- put("flt" ,TypeFloat .F64);
- put("i16" ,TypeInteger.I16);
- put("i32" ,TypeInteger.I32);
- put("i64" ,TypeInteger.BOT);
- put("i8" ,TypeInteger.I8 );
- put("int" ,TypeInteger.BOT);
- put("u1" ,TypeInteger.U1 );
- put("u16" ,TypeInteger.U16);
- put("u32" ,TypeInteger.U32);
- put("u8" ,TypeInteger.U8 );
- put("val" ,Type.TOP); // Marker type, indicates type inference
- put("var" ,Type.BOTTOM); // Marker type, indicates type inference
- }};
-
-
- public Parser(CodeGen code ) {
- _code = code;
- }
-
- @Override
- public String toString() { return _lexer==null ? "" : _lexer.toString(); }
-
- // Debugging utility to find a Node by index
- public Node f(int nid) { return _code.f(nid); }
-
- // Read and set CTRL
- private Node ctrl() { return _scope.ctrl(); }
- private <N extends Node> N ctrl(N n) { return _scope.ctrl(n); }
- // Read and set Memory
- private Node mem() { return _scope.mem(1); }
- private void mem(Node mem) { _scope.mem(mem); }
- // Structural label for precise memory Phis. The '$' prefix cannot be
- // referenced in Simple source.
- public static String memName(int alias) { return ("$"+alias).intern(); }
-
- // Ordinary parsing carries one whole-memory value. Alias partitioning is
- // graph optimization, not parser state.
- private void storeMem( Node st, int alias, Node prior ) {
- mem(alias==1 ? st : mergeAlias(prior,alias,st));
- }
-
- // A semantic graph partition representing the complete memory after a
- // precise alias update. ScopeNode treats this as one opaque value.
- private Node mergeAlias(Node bulk, int alias, Node precise) {
- assert bulk != null : "Missing bulk memory for alias #"+alias;
- MemMergeNode merge = new MemMergeNode(null,bulk);
- merge.alias(alias,precise);
- return peep(merge);
- }
-
-
- public Ary<FRefNode> parse( CompUnit ref ) {
- assert _scope == null && _breakScope == null && _continueScope == null && _returnScope == null;
- assert _frefs == null;
- _lexer = new Lexer(ref);
- _frefs = new Ary<>(FRefNode.class);
- _requiredTypes.clear();
- // Starting Scope has control, memory, initial arguments
- _scope = new ScopeNode();
- _scope.define(ScopeNode.CTRL, Type.CONTROL , false, null, _lexer);
- _scope.define(ScopeNode.MEM0, TypeMem.BOT , false, null, _lexer);
- // Track active scopes for Graph display
- _xScopes.push(_scope);
- ctrl(_code.XCTRL);
- mem(con(TypeMem.BOT));
-
- // File-level struct declaration
- _ref = ref;
- _nestedType = ref._cname;
- String typeName = addClzPrefix(ref._cname);
-
- ReturnNode clzret = parseStruct( true, typeName );
-
- if( !_lexer.isEOF() ) throw _errorSyntax("unexpected");
-
- // All still-unresolved forward refs
- // should immediately resolve to a file-based name which is a Simple
- // class type.
- Ary<FRefNode> frefs = new Ary<>(FRefNode.class);
- for( FRefNode fref : _frefs ) {
- if( fref.nIns() != 1 && fref._con == FRefNode.FREF_TYPE ) continue;
- CompUnit cu = ParseAll.findCompUnitOrThrow(_code,_ref,fref._name);
- fref._con = TypeMemPtr.make((byte)2,TypeStruct.make(addClzPrefix(cu._cname),true),true);
- frefs.push(fref);
- }
-
- // The class being published
- ref._clz = (TypeStruct)TYPES.get(typeName);
-
- // Clean up and reset
- _xScopes.pop();
- _scope.kill();
- _scope = null;
- _frefs = null;
-
- return frefs;
- }
-
-
- /**
- * Parses a function body, assuming the header is parsed.
- */
- private ReturnNode parseFunctionBody( TypeFunPtr sig, Lexer loc, String[] ids) {
- return parseFunctionBody(sig, loc, null, ids);
- }
-
- private ReturnNode parseFunctionBody( TypeFunPtr sig, Lexer loc, java.util.BitSet finals, String[] ids) {
- // Record & restore the existing scope vars. The function parse might
- // update fields, so when the parse is done reset back to the pre-parse
- // state.
- Node[] preParse = _scope.save();
-
- // Parse function body normally
- ReturnNode ret = _parseFunctionBody(null, sig,loc,finals,ids);
-
- // Reset all fields to pre-parse days
- _scope.restore(preParse);
- return ret;
- }
-
-
- // Parse a function body
-
- // Stack the {break,continue,return} scopes and a new lexical scope.
- // Build a FunNode and parameters, extending scope by parm names.
- // Parse the function body
- // - while( not end-of-function )
- // - - parseStatement
- // - - If isInit, also incrementally extend the self-type.
- // If isInit
- // - upgrade and close the self-type
- // - Gather fields and store values
- // - <init> returns self, not last
- // Unstack the {break,continue,return} scopes.
-
- private ReturnNode _parseFunctionBody( String funName, TypeFunPtr sig, Lexer loc, String[] ids) {
- return _parseFunctionBody(funName, sig, loc, null, ids);
- }
-
- private ReturnNode _parseFunctionBody( String funName, TypeFunPtr sig, Lexer loc, java.util.BitSet finals, String[] ids) {
- Constructor outerCtor=_pendingCtor;
- _pendingCtor=null;
- boolean isInit = FunNode.isInit(funName);
- boolean isCtor = isInit || isExplicitConstructorName(funName);
- String typeName = isCtor ? ((TypeMemPtr)sig._sig[0])._obj._name : null;
-
- // Stack parser state on the local Java stack, and unstack it later
- _scope.push(new Kind.Func(funName,isCtor && !funName.endsWith(".<clinit>")));
- ScopeNode breakScope = _breakScope; _breakScope = null;
- ScopeNode continueScope = _continueScope; _continueScope = null;
- ScopeNode returnScope = _returnScope; _returnScope = null;
-
- int oldUID = _code.UID(); // Used to approximate function size
- FunNode fun = (FunNode)peep(new FunNode(loc(),sig, funName, _ref, null, _ref._start));
- // Once the function header is available, install in linker table -
- // allowing recursive functions. Linker matches on declared args and
- // exact fidx, and ignores the return (because the fidx will only match
- // the exact single function).
- _code.link(fun);
-
- Node rpc = new ParmNode("$rpc",0,TypeRPC.BOT,fun,ConstantNode.seed(TypeRPC.BOT).peephole()).peephole();
-
- // Pre-call the function from Start, with worse-case arguments. This
- // represents all the future, yet-to-be-parsed functions calls and
- // external calls.
- _scope.ctrl(fun); // Scope control from function
- // Private mem alias tracking per function
- Node defaultMem = new ProjNode(_ref._start,1,ScopeNode.MEM0).peephole();
- Node privMem = new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun,defaultMem).peephole();
- mem(privMem);
- // All args, "as-if" called externally
- for( int i=0; i<ids.length; i++ ) {
- Type t = sig.arg(i);
- // Args take a default input if called from Start/external-world
- _scope.define(ids[i], t, (i==0 && ids[0]=="self") || (finals!=null && finals.get(i)), new ParmNode(ids[i],i+2,t,fun,ConstantNode.seed(fun.defaultArg(i)).peephole()).peephole(), loc);
- }
-
- if( isCtor && !isInit ) {
- int lex=_scope.klast()._lexSize;
- TypeStruct shape=(TypeStruct)TYPES.get(typeName);
- for( Field fld : shape._fields ) {
- int idx=fieldVarIdxBefore(lex,fld._fname);
- if( idx<0 ) continue;
- Var v=_scope.var(idx);
- if( v._fref || v._extern!=0 || v._uninit || _scope.in(v)._type.isConstant() ) continue;
- Node value=convertExpr(
- new LoadNode(loc,fld._fname,fld._alias,ctrl(),fun.parm(3),fun.parm(2),off(shape,fld._fname)).peephole(),v.type());
- _scope.update(v,value);
- }
- }
-
- // Each initializer establishes its parent before setting its own
- // run-once flag. Keep this in the body so every execution strategy
- // shares the same ordering, including direct entry into a child.
- if( fun.isClz() ) {
- if( _ref._par!=null ) {
- String pname = addClzPrefix(_ref._par._cname);
- TypeStruct parent = (TypeStruct)TYPES.get(pname);
- if( parent==null ) parent=TypeStruct.make(pname,true);
- Node ptr = con(TypeMemPtr.make((byte)2,parent,true)).keep();
- classInit(loc,ptr);
- ptr.unkeep();
- }
- Node self = fun.parm(2);
- Field flag = ((TypeMemPtr)self._type)._obj.field("$init");
- Node prior = mem().keep();
- Node st = peep(new StoreNode(loc,"$init",flag._alias,flag._t,ctrl(),prior,self,
- fldoff(self,"$init"),con(1),false));
- storeMem(st,flag._alias,prior);
- prior.unkeep();
- }
-
- // Parse the function body.
- Node last=_code.ZERO; // Last statement as the default result
-
- int nvar = 0;
- while (!peek('}') && !_lexer.isEOF()) {
- // Parse a statement; record last statement as default return
- last = _code.add(parseStatement());
- // For <init> and <clinit>, local vars are really struct fields and
- // get exposed as discovered. This allows late local functions to
- // use early declared fields.
- if( isInit )
- nvar = updateSelfAsFieldsDiscovered( typeName, nvar );
- }
-
- if( isInit ) finishConstructor();
- _pendingCtor=outerCtor;
-
- ParmNode self = fun.parm(2);
- // Last expression is the return
- if( ctrl()._type==Type.CONTROL ) {
- // Constructors get special return types
- if( fun.isClz() ) {
- assert last!=null;
- } else if( isCtor ) {
- last = fun.parm(3);
- }
- addReturn(last);
- // exit path is now dead (nothing live after a "return")
- }
- if( isCtor )
- upgradeSelfTypeAndStoreFields( typeName, fun.isClz(), self, fun.parm(3), isInit );
-
- // Build a return from the _returnScope.
- // Can be no returns for never-exit functions
- Node rctl = _returnScope==null ? _code.XCTRL : _returnScope.ctrl().peephole();
- Node rmem = rctl==_code.XCTRL ? con(TypeMem.TOP) : _returnScope.mem();
- Node expr = rctl==_code.XCTRL ? con(Type.TOP) : _returnScope._inputs.last().peephole();
- ReturnNode ret = (ReturnNode)peep(new ReturnNode(rctl, rmem, expr, rpc, fun));
- fun.setRet(ret);
- // Approximate function size, for inlining heuristics
- fun._approxUIDs = _code.UID() - oldUID;
- for( Node cend : ret.outs() )
- if( cend instanceof CallEndNode )
- throw TODO(); //_code.add(cend); // have call ends ALREADY? just barely made the Return
-
- // Export (and keep-alive) functions until prove dead
- _ref.addFun(_code,fun);
-
- // Unstack parser state
- if( _returnScope != null ) _returnScope.kill();
- _returnScope = returnScope;
- _continueScope = continueScope;
- _breakScope = breakScope;
- _scope.pop();
-
- return ret;
- }
-
- private int updateSelfAsFieldsDiscovered( String typeName, int nvar ) {
- int lex = _scope.klast()._lexSize + 2/*Skip self and selfMem*/;
- // Load self type in and out of the TYPES hashtable.
- TypeStruct tself = (TypeStruct)TYPES.get(typeName);
- // Start looking for newly declared vars at the lexical scope start,
- // skipping two ("self" and "selfmem") and fields seen already.
- while( lex + nvar < _scope.nIns() ) {
- Var v = _scope.var(lex + nvar++);
- // Field with sharper type
- Field old = tself.field(v._name);
- Field fld = v._extern!=0
- ? Field.make(v._name,v.type(),v._extern,false,true)
- : old == null
- ? Field.make(v._name,v.type(),_code.alias(_ref._cname),v._final)
- : old.makeFrom(v.type());
- tself = tself.addOrUpdate(fld);
- }
- TYPES.put(typeName,tself);
- return nvar;
- }
-
-
- void upgradeSelfTypeAndStoreFields( String typeName, boolean isClz, ParmNode self, ParmNode smem, boolean upgrade ) {
- int lex = _scope.klast()._lexSize;
- assert _scope.var(lex)._name=="self";
- TypeStruct tself = (TypeStruct)TYPES.get(typeName);
- int base = lex+2; // Skip ctrl, mem
- if( upgrade ) {
- // Default <init>/<clinit> defines the struct and can sharpen fields.
- // A named constructor only initializes the already-closed struct.
- for( int i=0; i<tself.nkids(); i++ ) {
- Field tfld = tself._fields[i];
- int vidx = fieldVarIdx(base,tfld._fname);
- if( vidx == -1 )
- continue;
- Var v = _scope.var(vidx);
- // Forward-ref fields where placed in tself but will get promoted
- // to an outer scope - and should not be in tself.
- if( v._fref )
- tself = tself.remove(i--);
- else if( v.type() != tfld._t ) {
- assert v.type().isa(tfld._t);
- tself = tself.replace( tfld.makeFrom( v.type() ) );
- }
- }
-
- // Close Struct type after parsing struct body.
- TYPES.put( tself._name, tself = tself.close() );
- // Improve self, selfMem types in scope.
- // Class self-type is a singleton pointer. Instance <init> receives the
- // broad allocation pointer, while private memory tracks initialized fields.
- self._declaredType = self._type = isClz ? TypeMemPtr.make((byte)2,tself,true) : TypeMemPtr.make(constructorRecv(tself));
- FunNode fun = self.fun();
- fun.setSig(fun.sig().makeFrom(self._type,0));
- if( !isClz ) // <init> returns upgraded private memory
- smem._declaredType = smem._type = TypeMem.makePrivate(tself);
- }
-
- if( !upgrade ) {
- IdentityHashMap<Var,Type> exit = null;
- if( _returnScope!=null && !_returnScope.ctrl()._type.isHigh() ) {
- exit = new IdentityHashMap<>();
- int first = _scope._kinds.at(_scope.depth()-2)._lexSize+2;
- for( int i=first; i<lex; i++ )
- exit.put(_scope.var(i),_returnScope.in(i)._type);
- }
- _ctorExits.add(exit);
- } else if( !isClz ) {
- checkInitFields(tself,base);
- }
-
- // When can _returnScope be null here? A never-exit constructor will
- // not have any returns, and thus no need to gather values and store
- // them into the (never) constructed object
- if( _returnScope==null )
- return;
-
- // A MemMerge to gather field updates as stores. Classes update the
- // normal public memory. <init> updates the private memory that got
- // passed in - is treated like a normal argument and not like memory.
- Node mmm = isClz ? _returnScope.mem(1) : smem;
-
-
- // Store constructor results into fields
- for( int i=0; i<tself.nkids(); i++ ) {
- Field fld = tself._fields[i];
- int vidx = upgrade
- ? fieldVarIdx(base,fld._fname)
- : fieldVarIdxBefore(lex,fld._fname);
- if( vidx == -1 )
- continue;
- Var v = _scope.var(vidx);
- if( v._fref || v._extern!=0 ) continue; // C storage has no Simple initializer
- // NewNode's TOP field is the zero-code poison for a required
- // field. Leave it alone unless the declaration body supplied a
- // value; otherwise a user constructor must initialize it.
- Node val = _returnScope.in(vidx);
- if( upgrade && v._uninit &&
- !initialized(val._type) ) continue;
- // A numeric merge can still be untyped while parsing. Retain its
- // declared conversion at the helper boundary, as assignments do.
- if( fld._t instanceof TypeInteger || fld._t instanceof TypeFloat )
- val=convertExpr(val,fld._t);
- // Store value into extended struct
- Node prior = mmm.keep();
- Node st = peep(new StoreNode(null, fld._fname, fld._alias, fld._t, null, prior, self, off(tself,fld._fname), val, fld._final));
- mmm = mergeAlias(prior,fld._alias,st);
- prior.unkeep();
- }
- mmm = mmm.peephole();
-
- if( !isClz )
- // Stuff private mem into normal expression return value.
- _returnScope.setDef(_returnScope.nIns()-1,mmm);
- else
- // Parser memory is one opaque whole-memory edge. Publish the
- // completed class-field aggregate as the <clinit> return memory.
- _returnScope.mem(mmm);
- //_code.add(mmm);
- }
-
- // Validate each user constructor, or an implicit empty one, before the
- // declaration scope goes away. Later fields use their declaration defaults.
- private static boolean initialized(Type t) {
- return t!=Type.BOTTOM && !t.isHigh() && (!(t instanceof TypeNil tn) || tn.notNull());
- }
-
- private void checkInitFields(TypeStruct ts, int base) {
- if( _returnScope==null || _returnScope.ctrl()._type.isHigh() ) return;
- boolean implicit = _ctorExits.isEmpty();
- if( implicit ) _ctorExits.add(new IdentityHashMap<>());
- for( IdentityHashMap<Var,Type> exit : _ctorExits ) {
- if( exit==null ) continue;
- for( Field fld : ts._fields ) {
- int idx = fieldVarIdx(base,fld._fname);
- if( idx == -1 ) continue;
- Var v = _scope.var(idx);
- if( !v._uninit || v._fref ) continue;
- Type t = exit.getOrDefault(v,_returnScope.in(idx)._type);
- if( !initialized(t) )
- throw error("'"+ts._name+"' is not fully initialized, field '"+fld._fname+
- (implicit ? "' needs to be set in a constructor" :
- "' is only partially set in the constructor"),v._loc);
- }
- }
- }
-
- private int fieldVarIdx( int base, String name ) {
- for( int i=base; i<_scope.nIns(); i++ )
- if( _scope.var(i)._name == name )
- return i;
- return -1;
- }
-
- // Named constructors update field Vars from the enclosing initializer
- // scope. Constructor arguments and locals begin at limit and must not
- // shadow the field state gathered into private memory.
- private int fieldVarIdxBefore( int limit, String name ) {
- for( int i=limit-1; i>=0; i-- )
- if( _scope.var(i)._name == name )
- return i;
- return -1;
- }
-
- private boolean isConstructorDecl(String name) {
- TypeStruct self = _scope.constructorSelf();
- return self != null && name == initFieldName(self._name);
- }
-
- private boolean inExplicitConstructor() {
- Kind.Func func = (Kind.Func)_scope._kinds.at(_scope.enclosingFuncOrDecl());
- return isExplicitConstructorName(func._name);
- }
-
- private boolean isExplicitConstructorName(String funName) {
- TypeStruct self = _scope.constructorSelf();
- return self != null && funName == (self._name+"."+initFieldName(self._name)).intern();
- }
-
- private static TypeStruct constructorRecv(TypeStruct self) {
- Field[] fs = new Field[self._fields.length];
- for( int i=0; i<fs.length; i++ ) {
- Field fld = self._fields[i];
- fs[i] = fld.makeFrom(fld._t.makeStorage());
- }
- return TypeStruct.make(self._name,self._open,fs);
- }
-
-
- /**
- * Parses a block
- *
- * <pre>
- * '{' statements '}'
- * </pre>
- * Does not parse the opening or closing '{}'
- * @return a {@link Node} or {@code null}
- */
- private Node parseBlock(Kind kind) {
- // Enter a new scope
- _scope.push(kind);
- Node last = _code.ZERO;
- while (!peek('}') && !_lexer.isEOF()) {
- _code.add(last);
- last = parseStatement();
- }
- // Exit scope
- last.keep();
- _scope.pop();
- return last.unkeep();
- }
-
- /**
- * Parses a statement
- *
- * <pre>
- * returnStatement | declStatement | blockStatement | ifStatement | expressionStatement
- * </pre>
- * @return a {@link Node} or {@code null}
- */
- private Node parseStatement() {
- if( matchx("return") ) return parseReturn();
- else if (matchx("if") ) return parseIf();
- else if (matchx("while") ) return parseWhile();
- else if (matchx("for") ) return parseFor();
- else if (matchx("break") ) return parseBreak();
- else if (matchx("continue")) return parseContinue();
- else if (matchx("struct") ) return parseStruct();
- else if (inStructDeclaration() && matchx("new")) return parseConstructorDeclaration();
- else if (matchx(";") ) return _code.ZERO; // Empty statement
- // Break ambiguity around leading function types and starting a block
- else if (peek('{') && !isTypeFun() ) {
- match("{");
- return require(parseBlock(new Kind.Block()),"}");
- }
- // Declaration or normal assignment/expression
- else return parseDeclarationStatement();
- }
-
- private boolean inStructDeclaration() {
- return _scope.klast() instanceof Kind.Func fun && FunNode.isInstance(fun._name);
- }
-
- // Constructor declaration: new TypeName = { args -> body };
- private record Constructor(String name, Lexer body) {}
- private Constructor _pendingCtor;
-
- private Node parseConstructorDeclaration() {
- TypeStruct self = _scope.constructorSelf();
- assert self != null;
- String name = requireId();
- String expect = initFieldName(self._name);
- if( name != expect )
- throw error("Constructor name '" + name + "' must match type name '" + expect + "'");
- require("=");
- if( _pendingCtor!=null ) throw error("Redefining constructor '"+name+"'");
- _pendingCtor=new Constructor(name,loc());
- // Parse the body after all field declarations are known, so self.x
- // and x share a local slot even when x is declared below this body.
- require("{");
- int depth=1;
- while( depth>0 ) {
- _lexer.skipWhiteSpace();
- if( _lexer.isEOF() ) throw error("Unclosed constructor body");
- char ch=_lexer.nextChar();
- if( ch=='{' ) depth++;
- else if( ch=='}' ) depth--;
- else if( ch=='"' || ch=='\'' ) {
- char quote=ch;
- while( !_lexer.isEOF() ) {
- ch=_lexer.nextChar();
- if( ch=='\\' ) _lexer.nextChar();
- else if( ch==quote ) break;
- }
- }
- }
- require(";");
- return _code.ZERO;
- }
-
- private void finishConstructor() {
- if( _pendingCtor==null ) return;
- Constructor ctor=_pendingCtor;
- _pendingCtor=null;
- Lexer after=_lexer;
- _lexer=new Lexer(ctor.body());
- defineConstructor(ctor.name());
- _lexer=after;
- }
-
- private void defineConstructor(String name) {
- TypeStruct self=_scope.constructorSelf();
- Node ctor = constructorFunc(name);
- TypeFunPtr sig = (TypeFunPtr)ctor._type;
- String clzName = addClzPrefix(self._name);
- TypeStruct clz = (TypeStruct)TYPES.get(clzName);
- Field old = clz==null ? null : clz.field("<ctor>");
- if( old != null )
- throw error("Redefining constructor '" + name + "'");
- Field fld = Field.make("<ctor>",sig,_code.alias(_ref._cname),true);
- TYPES.put(clzName,clz==null ? TypeStruct.make(clzName,true,fld) : clz.add(fld));
- require(ctor,";");
- }
-
- /**
- * Parses a while statement
- *
- * <pre>
- * while ( expression ) statement
- * </pre>
- * @return a {@link Node}, never {@code null}
- */
- private Node parseWhile() {
- require("(");
- return parseLooping(false);
- }
-
-
- /**
- * Parses a for statement
- *
- * <pre>
- * for( var x=init; test; incr ) body
- * </pre>
- * @return a {@link Node}, never {@code null}
- */
- private Node parseFor() {
- // { var x=init,y=init,...;
- // while( test ) {
- // body;
- // next;
- // }
- // }
- require("(");
- _scope.push(new Kind.Block()); // Scope for the index variables
- if( !match(";") ) // Can be empty init "for(;test;next) body"
- parseDeclarationStatement(); // Non-empty init
- Node rez = parseLooping(true);
- _scope.pop(); // Exit index variable scope
- return rez;
- }
-
- // Shared by `for` and `while`
- private Node parseLooping( boolean doFor ) {
-
- var savedContinueScope = _continueScope;
- var savedBreakScope = _breakScope;
-
- // Loop region has two control inputs, the first is the entry
- // point, and second is back edge that is set after loop is parsed
- // (see end_loop() call below). Note that the absence of back edge is
- // used as an indicator to switch off peepholes of the region and
- // associated phis; see {@code inProgress()}.
-
- ctrl(new LoopNode(loc(),null,ctrl()).peephole()); // Note we set back edge to null here
-
- // At loop head, we clone the current Scope (this includes all
- // names in every nesting level within the Scope).
- // We create phis eagerly for all the names we find, see dup().
-
- // Save the current scope as the loop head
- ScopeNode head = _scope.keep();
- // Clone the head Scope to create a new Scope for the body.
- // Create phis eagerly as part of cloning
- _xScopes.push(_scope = _scope.dup(true)); // The true argument triggers creating phis
-
- // Parse predicate
- var pred = peek(';') ? con(1) : parseAsgn();
- require( doFor ? ";" : ")" );
-
- // IfNode takes current control and predicate
- Node ifNode = new IfNode(ctrl(), pred.keep()).peephole();
- // Setup projection nodes
- CFGNode ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep();
- CFGNode ifF = (CFGNode)new CProjNode(ifNode.unkeep(), 1, "False").peephole();
-
- // for( ;;next ) body
- int nextPos = -1, nextEnd = -1;
- if( doFor ) {
- // Skip the next expression and parse it later
- nextPos = pos();
- skipAsgn();
- nextEnd = pos();
- require(")");
- }
-
- // Clone the body Scope to create the break/exit Scope which accounts for any
- // side effects in the predicate. The break/exit Scope will be the final
- // scope after the loop, and its control input is the False branch of
- // the loop predicate. Note that body Scope is still our current scope.
- ctrl(ifF);
- _xScopes.push(_breakScope = _scope.dup());
- _breakScope.addGuards(ifF,pred,true); // Up-cast predicate
-
- // No continues yet
- _continueScope = null;
-
- // Parse the true side, which corresponds to loop body
- // Our current scope is the body Scope
- ctrl(ifT.unkeep()); // set ctrl token to ifTrue projection
- _scope.addGuards(ifT,pred.unkeep(),false); // Up-cast predicate
- parseStatement().isKill(); // Parse loop body
- _scope.removeGuards(ifT);
-
- // Merge the loop bottom into other continue statements
- if (_continueScope != null) {
- _continueScope = jumpTo(_continueScope);
- _scope.kill();
- _scope = _continueScope;
- }
-
- // Now append the next code onto the body code
- if( doFor ) {
- int old = pos(nextPos);
- if( !peek(')') )
- parseAsgn();
- if( pos() != nextEnd )
- throw errorSyntax( "Unexpected code after expression" );
- pos(old);
- }
-
- // The true branch loops back, so whatever is current _scope.ctrl gets
- // added to head loop as input. endLoop() updates the head scope, and
- // goes through all the phis that were created earlier. For each phi,
- // it sets the second input to the corresponding input from the back
- // edge. If the phi is redundant, it is replaced by its sole input.
- var exit = _breakScope;
- head.balanceLoopFRefs(_scope);
- exit.balanceLoopFRefs(_scope);
- head.endLoop(_scope, exit);
- head.unkeep().kill();
-
- _xScopes.pop(); // Cleanup
- _xScopes.pop(); // Cleanup
-
- _continueScope = savedContinueScope;
- _breakScope = savedBreakScope;
-
- // At exit the false control is the current control, and
- // the scope is the exit scope after the exit test.
- // During sys parsing, there is no xscope here.
- if( !_xScopes.isEmpty() ) {
- _xScopes.pop();
- _xScopes.push( exit );
- }
- _scope = exit;
- return _code.ZERO;
- }
-
- private ScopeNode jumpTo(ScopeNode toScope) {
- ScopeNode cur = _scope.dup();
- ctrl(_code.XCTRL); // Kill current scope
- // Prune nested lexical scopes that have depth > than the loop head
- // We use _breakScope as a proxy for the loop head scope to obtain the depth
- while( cur.depth() > _breakScope.depth() )
- cur.pop();
- // If this is a continue then first time the target is null
- // So we just use the pruned current scope as the base for the
- // "continue"
- if( toScope == null )
- return cur;
- // toScope is either the break scope, or a scope that was created here
- assert toScope.depth() <= _breakScope.depth();
- toScope.ctrl(toScope.mergeScopes(cur,loc()).peephole());
- return toScope;
- }
-
- private void checkLoopActive() { if (_breakScope == null) throw error("No active loop for a break or continue"); }
-
- private Node parseContinue() { checkLoopActive(); _continueScope = require(jumpTo( _continueScope ),";"); return _code.ZERO; }
- private Node parseBreak () {
- checkLoopActive();
- // At the time of the break, and loop-exit conditions are only valid if
- // they are ALSO valid at the break. It is the intersection of
- // conditions here, not the union.
- _breakScope.removeGuards(_breakScope.ctrl());
- _breakScope = require(jumpTo(_breakScope ),";");
- _breakScope.addGuards((CFGNode)_breakScope.ctrl(), null, false);
- return _code.ZERO;
- }
-
- // Look for an unbalanced `)`, skipping balanced
- private void skipAsgn() {
- int paren=0;
- while( true )
- // Next X char handles skipping complex comments
- switch( _lexer.nextXChar() ) {
- case Character.MAX_VALUE:
- throw TODO();
- case ')':
- if( --paren<0 ) {
- posT( pos() - 1 );
- return; // Leave the `)` behind
- }
- break;
- case '(': paren ++; break;
- default: break;
- }
- }
-
-
- /**
- * Parses a statement
- *
- * <pre>
- * if ( expression ) statement [else statement]
- * </pre>
- * @return a {@link Node}, never {@code null}
- */
- private Node parseIf() {
- // Parse predicate
- require("(");
- var pred = require(parseAsgn(), ")");
- return parseTrinary(pred,"else");
- }
-
- // Parse a conditional expression, merging results.
- private Node parseTrinary( Node pred, String fside ) {
- // IfNode takes current control and predicate
- pred.keep();
- Node ifNode = new IfNode(ctrl(), pred).peephole();
- // Setup projection nodes
- CFGNode ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep();
- CFGNode ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole().keep();
- // In if true branch, the ifT proj node becomes the ctrl
- // But first clone the scope and set it as current
- ScopeNode fScope = _scope.dup(); // Duplicate current scope
- fScope.ctrl(ifF); // The pending arm already starts at False
- _xScopes.push(fScope); // For graph visualization we need all scopes
-
- // Parse the true side
- ctrl(ifT.unkeep()); // set ctrl token to ifTrue projection
- _scope.addGuards(ifT,pred,false); // Up-cast predicate
- // Parse true-side flavor
- Node lhs = switch( fside ) {
- case "else" -> parseStatement(); // if( pred ) stmts;
- case ":" -> parseAsgn(); // pred ? asgn;
- case "&&" -> parseLogical(); // pred && expr
- case "||" -> pred; // pred || expr_is_ignored
- default -> throw TODO();
- };
- lhs.keep();
- _scope.removeGuards(ifT);
-
- // See if a one-sided def was made: "if(pred) int x = 1;" and throw.
- // See if any forward-refs were made, and copy them to the other side:
- // "pred ? n*fact(n-1) : 1"
- fScope.balanceIf(_scope);
-
- ScopeNode tScope = _scope;
-
- // Parse the false side
- _scope = fScope; // Restore scope, then parse else block if any
- ifF.unkeep(); // fScope already owns the false control
- // Up-cast predicate, even if not else clause, because predicate can
- // remain true if the true clause exits: `if( !ptr ) return 0; return ptr.fld;`
- _scope.addGuards(ifF,pred,true);
- // Parse false-side flavor
- boolean doRHS = false; // RHS is optional for if/else and trinary
- Node rhs = switch( fside ) {
- case "else" -> (doRHS=match(fside)) ? parseStatement() : con(lhs._type.makeZero());
- case ":" -> (doRHS=match(fside)) ? parseAsgn() : con(lhs._type.makeZero());
- case "&&" -> con(lhs._type.makeZero()); // pred && expr_is_ignored
- case "||" -> parseLogical(); // pred || expr
- default -> throw TODO();
- };
- rhs.keep();
- _scope.removeGuards(ifF);
- // The RHS of `||` is parsed on the false side, so retain its updated
- // control, memory and locals just as an explicit else arm does.
- if( doRHS || fside.equals("||") )
- fScope = _scope;
- pred.unkeep();
-
- // Check the trinary widening int/flt
- if( !fside.equals("else") ) {
- rhs = widenInt( rhs.unkeep(), lhs._type ).keep();
- lhs = widenInt( lhs.unkeep(), rhs._type ).keep();
- }
-
- _scope = tScope;
- _xScopes.pop(); // Discard pushed from graph display
- // See if a one-sided def was made: "if(pred) int x = 1;" and throw.
- // See if any forward-refs were made, and copy them to the other side:
- // "pred ? n*fact(n-1) : 1"
- tScope.balanceIf(fScope);
-
- // Merge results
- RegionNode r = ctrl(tScope.mergeScopes(fScope,loc()));
- Node ret = peep(new PhiNode("",r,lhs.unkeep(),rhs.unkeep()));
- // Immediately fail e.g. `arg ? 7 : ptr`
- ParseException err;
- if( !fside.equals("else") && (err=ret.err()) !=null ) throw err;
- ret.keep();
- r.peephole();
- return ret.unkeep();
- }
-
- /**
- * Parses a return statement; "return" already parsed.
- * The $ctrl edge is killed.
- *
- * <pre>
- * 'return' expr ;
- * </pre>
- * @return an expression {@link Node}, never {@code null}
- */
- private Node parseReturn() {
- var expr = require(parseAsgn(), ";");
- return addReturn(expr);
- }
- private Node addReturn(Node expr) {
- expr.keep();
- // Need default memory, since it can be lazy, need to force
- // a non-lazy Phi
- mem(); // Force a pending lazy bulk-memory Phi
- // For <init> and <clinit> - all fields in scope will be live-on-exit
- // and stored into `self` and need to have lazy-phi's inserted.
- int lexN = _scope.enclosingFuncOrDecl();
- Kind.Func k = (Kind.Func)_scope._kinds.at(lexN);
- if( FunNode.isInit(k._name) ) {
- int nestedLexSize = lexN+1 < _scope.depth() ? _scope._kinds.at(lexN+1)._lexSize : _scope.nIns();
- for( int i=k._lexSize; i<nestedLexSize; i++ )
- _scope.update(_scope.var(i),null);
- if( _returnScope != null ) {
- Var vexpr = _returnScope._vars.pop();
- Node oldX = _returnScope.removeLast();
- int rlen = _returnScope.nIns();
- while( rlen < nestedLexSize ) {
- _returnScope._vars.add(_scope.var(rlen));
- _returnScope.addDef(con(_scope.var(rlen++).type().makeZero()));
- }
- _returnScope._vars.add(vexpr);
- _returnScope.addDef(oldX);
- }
- } else if( isExplicitConstructorName(k._name) ) {
- // Field bindings live in the enclosing declaration, not among the
- // constructor's arguments/locals. Force lazy Phis on every exit.
- int first = _scope._kinds.at(lexN-1)._lexSize+2;
- for( int i=first; i<k._lexSize; i++ )
- _scope.update(_scope.var(i),null);
- }
-
- // No prior merge point? Just clone and hang on to it
- if( _returnScope == null ) {
- _returnScope = _scope.dup();
- while( lexN+1 < _returnScope.depth() )
- _returnScope._pop(); // Pop a nested block scope until we hit the function scope
- _returnScope.define("$expr", inferredReturnType(expr._type), true, expr, null);
-
- } else {
- // For <init> and <clinit> - ALL FIELDS IN LAST SCOPE are live
- // and need to have lazy-phi's inserted.
-
- // And fields might not match.... will need matching Var/define with default values
- int rlen = _returnScope.nIns()-1;
- RegionNode r = ctrl(new RegionNode(null, null,_returnScope.ctrl(), _scope.ctrl()).init().keep());
- _returnScope._merge(_scope, r, rlen);
- Node oldExpr = _returnScope.in(rlen);
- _returnScope.setDef(rlen,new PhiNode("$expr", r, oldExpr, expr).peephole());
- _code.add(r);
- _returnScope.ctrl(r.unkeep());
- }
- ctrl(_code.XCTRL); // Kill control
- return expr.unkeep();
- }
-
- /** Parse: [name '='] expr
- */
- private Node parseAsgn() {
- int old = pos();
- String name = _lexer.matchId();
- // Just a plain expression, no assignment.
- // Distinguish `var==expr` from `var=expr`
- if( name==null || KEYWORDS.contains(name) || !matchOpx('=','=') )
- { pos(old); return parseExpression(); }
-
- // Find variable to update
- Var def = _scope.lookup(name);
- if( def==null )
- def = defineOpenConstructorField(name);
- if( def==null )
- throw error("Undefined name '" + name + "'");
-
- if( def._extern!=0 ) // Parse the C binding as a memory lvalue
- { pos(old); return parseExpression(); }
-
- // TOP fields are for late-initialized fields; these have never
- // been written to, and this must be the final write. Other writes
- // outside the constructor need to check the final bit.
- if( _scope.in(def._idx)._type!=Type.TOP && def._final &&
- // Inside an allocation, final assign is OK, outside nope.
- // The alloc() call added the allocation scope
- !_scope.canInit(def) )
- throw error("Cannot reassign final '"+name+"'");
- checkSharedFunction(def);
-
- // Parse assignment expression
- Node expr = parseAsgn();
-
- // Lift expression, based on type
- Type decl = def.type(); // Declared type
- Node lift = convertExpr(expr.keep(), decl).keep();
- // Update
- _scope.update(name,lift);
-
- // Check for storing into an instance field instead of a local var
- int kx = _scope.kindx(def); // Declaration scope
- int fx = _scope.enclosingFunction(); // Enclosing function scope
- Kind kk = _scope._kinds.at(kx);
- Kind fk = _scope._kinds.at(fx);
- // Access instance field from 'self'
- if( kx+1 == fx && kk instanceof Kind.Func inst && FunNode.isInstance(inst._name) &&
- fk instanceof Kind.Func method && !inExplicitConstructor() ) {
- // Named field offset on an unknown struct type
- TypeMemPtr self = (TypeMemPtr)_scope.var(fk._lexSize).type();
- TypeStruct sobj = (TypeStruct)TYPES.get(self._obj._name);
- Field fld = sobj.field(name);
- int alias = fld._alias;
- Node ptr = _scope.in(method._lexSize);
- Node off = fldoff(ptr,name);
- Node prior = mem().keep();
- Node st = new StoreNode(loc(), name, alias, decl, ctrl(), prior, ptr, off, lift, inExplicitConstructor()).peephole().keep();
- storeMem(st,alias,prior);
- st.unkeep();
- prior.unkeep();
- }
- lift.unkill();
-
-
- // Return un-lifted expr
- return expr.unkeep();
- }
-
- // Widen ints to floats; narrow wide int types.
- // Early error if types do not match variable.
- private Node convertExpr( Node expr, Type t ) {
- if( !expr._type.accessISA(t) )
- throw error("Type "+expr._type.str()+" is not of declared type "+t.str());
- if( t instanceof TypeMemPtr tmp && tmp.isFinal() )
- expr = peep(new ReadOnlyNode(expr));
- // A FunPtr carries the semantic edge to its function's Return. Do not
- // hide that edge behind a Convert which can fold to a plain constant.
- if( expr instanceof FunPtrNode && t instanceof TypeFunPtr )
- return expr;
- // Auto-widen array to i64 (cast ptr to raw int bits)
- if( t == TypeInteger.BOT && expr._type instanceof TypeMemPtr tmp && tmp._obj.isAry() )
- return peep(new AddNode(peep(new PtrToIntNode(expr)),off(tmp._obj,"[]")));
- // Widen ints to floats, narrow wide types
- return peep(new ConvertNode(t,expr));
- }
-
- // Inferred `var` declarations describe writable slots, not the exact
- // value used to initialize them. Explicitly typed locals retain the
- // user's nullability and numeric-width guarantees; `val` retains the
- // initializer's precise type.
- private static Type mutableInferredType(Type t) {
- return switch( t ) {
- case TypeInteger ti -> TypeInteger.BOT;
- case TypeFloat tf -> TypeFloat.F64;
- case TypeMemPtr tmp -> tmp.makeVar();
- case TypeFunPtr tfp -> tfp;
- case TypeConAry<?> a -> a;
- case TypePtr ptr -> TypePtr.PTR;
- case TypeScalar ts -> TypeScalar.BOT;
- default -> t;
- };
- }
-
- private Type constructorFieldType(Type t) {
- if( t instanceof TypeMemPtr ptr && ptr._obj.isConAry() ) {
- TypeStruct declared=(TypeStruct)TYPES.get(ptr._obj._name);
- TypeStruct shape=typeAry(declared._fields[1]._t,ptr._obj._fields[1]._final);
- return TypeMemPtr.make((byte)2,shape,false).withAccess(ptr._ro);
- }
- return mutableInferredType(t);
- }
-
- // Seed an inferred return merge with the declared numeric family instead
- // of a transient constant/range. Pointer nullability remains part of the
- // inferred function result and is combined by the later Phi.
- private static Type inferredReturnType(Type t) {
- return switch( t ) {
- case TypeInteger ti -> TypeInteger.BOT;
- case TypeFloat tf -> TypeFloat.F64;
- default -> t;
- };
- }
-
- private Node widenInt( Node expr, Type t ) {
- return (expr._type instanceof TypeInteger || expr._type==Type.NIL) && t instanceof TypeFloat
- ? peep(new ToFloatNode(expr)) : expr;
- }
-
- /**
- * Parse declaration or expression statement
- * declStmt = type var['=' exprAsgn][, var['=' exprAsgn]]* ';' | exprAsgn ';'
- * <p>
- * exprAsgn = var '=' exprAsgn | expr
- */
- private Node parseDeclarationStatement() {
- int old = pos();
- Type t = type();
- if( peek('.') ) // Ambiguity static vars: "type.var", parse as expression
- { pos(old); t=null; }
- if( t == null )
- return require(parseAsgn(),";");
-
- // now parse var['=' asgnexpr] in a loop
- Node n = parseDeclaration(t);
- while( match(",") )
- n = parseDeclaration(t);
- return require(n,";");
- }
-
- // The binding modifier sits immediately before the name. It never
- // changes permissions on the referenced object.
- private boolean bindingFinal(Type t) {
- if( match("!") ) return false;
- if( match("~") ) return true;
- return t==Type.TOP || t instanceof TypeNil;
- }
-
- /** Parse final: [!|~]var['=' asgn]
- */
- private Node parseDeclaration(Type t) {
- assert t!=null;
- // Has var/val instead of a user-declared type
- boolean inferType = t==Type.TOP || t==Type.BOTTOM;
- boolean xfinal = bindingFinal(t);
- Lexer loc = loc();
- String name = requireId();
- // Optional initializing expression follows
- boolean fld_final = false; // Field is final, but not deeply final
- boolean uninit = false; // Required non-null constructor field
- Node expr;
- if( match("=") ) {
- if( isExternDecl() ) {
- if( !(t instanceof TypeFunPtr) ) {
- Node ptr = externData(name,t);
- // The lexical binding is a fixed address; its contents are mutable.
- if( !_scope.define(name,t,true,ptr,loc) )
- throw error("Redefining name '"+name+"'",loc);
- _scope.lookup(name)._extern = _code.externAlias(name);
- return _code.ZERO;
- }
- expr = externDecl(name,t);
- t = expr._type; // Upgrade declared type to the exact extern decl type
- } else if( isConstructorDecl(name) ) {
- expr = constructorFunc(name);
- } else {
- expr = parseAsgn();
- }
- // var/val, then type comes from expression
- if( inferType ) {
- if( expr._type==Type.NIL )
- throw error("a not-null/non-zero expression");
- t = expr._type;
- if( !xfinal ) t = mutableInferredType(t);
- // Instance fields may change until the constructor finishes.
- // Keep the declared family/access, not the default's constant
- // value or string contents. Class constants remain static.
- else if( _scope.klast() instanceof Kind.Func fun && FunNode.isInstance(fun._name) && !(t instanceof TypeFunPtr) )
- t = constructorFieldType(t);
- }
-
- // expr is a constant function
- if( t instanceof TypeFunPtr decl && expr._type instanceof TypeFunPtr tfp && tfp.isConstant() ) {
- t = decl.makeFrom(tfp.fidx());
- FunNode fun = _code.link(tfp);
- if( fun != null )
- fun.setName(name); // Assign debug name to Simple function
- }
-
- } else {
- // Since no initializer, not-final but might need an initializer
-
- // Need an expression to infer the type.
- // Also, if not-null then need an initializing expression.
- // Allowed in a class def, because type/init will happen in the constructor.
- if( (inferType || (t instanceof TypeNil tn && !tn.nullable() )) && !_scope.inConstructor() )
- throw errorSyntax("=expression");
- // Initial value for uninitialized struct fields.
- expr = switch( t ) {
- // Required pointers retain their declared shape, but include
- // null until every constructor path assigns them.
- case TypeNil tn -> tn.nullable() ? con(Type.NIL) : con(t.meet(Type.NIL));
- case TypeInteger ti -> xfinal ? con(Type.BOTTOM) : _code.ZERO;
- case TypeFloat tf -> con(xfinal ? Type.BOTTOM : TypeFloat.FZERO);
- // Bottom signals type inference: they must be initialized in
- // the constructor and that's when we'll discover the type.
- case Type tt -> { assert tt==Type.BOTTOM; yield con(tt); }
- };
- // Every non-null pointer without a default is constructor-required.
- // Without `!` it is also shallow-final; mutability and definite
- // initialization are independent properties.
- if( (xfinal && !(t instanceof TypeNil)) || (t instanceof TypeNil tn && !tn.nullable()) ) {
- uninit = true;
- if( xfinal ) fld_final = true;
- }
- }
-
- // Lift expression, based on type
- // Do not let an authoritative Convert hide the nullable
- // not-yet-initialized state from constructor-local type flow.
- Node lift = uninit ? expr : convertExpr(expr, t);
-
- // Define a new name
- if( !_scope.define(name,t,xfinal || fld_final,lift, loc) )
- throw error("Redefining name '" + name + "'", loc);
- _scope.lookup(name)._uninit = uninit;
- return lift;
- }
-
-
- /**
- * Parse a struct declaration, and return the following statement.
- * Structs cannot be redefined, but can be nested.
- *
- * @return zero
- */
- private Node parseStruct( ) {
- // keyword "struct" already parsed, so expect the struct name next.
- String typeName = requireId();
- require("{");
-
- // create type class:NESTED.typeName; no fields.
-
- String old = _nestedType;
- String fullName = _scope.depth()==1 && _nestedType.endsWith(typeName) ? old : (old+"."+typeName).intern();
- _nestedType = fullName; // Recursive structs start with this as their basename
-
- ReturnNode ret = parseStruct( false, fullName );
-
- _nestedType = old;
-
- // Insert a field into class:NESTED.typeName of "val typeName = fcn-ptr-to-init"
- // Function does malloc internally.
- // Future self: means subtyping has to deal with "who does the malloc"
- String clzName = addClzPrefix(fullName);
- TypeFunPtr sig = ret.fun().sig();
- TypeStruct ts = (TypeStruct)TYPES.get(clzName);
- String initName = initFieldName(fullName);
- Field oldFld = ts==null ? null : ts.field(initName);
- Field fld = oldFld==null
- ? Field.make(initName,sig,_code.alias(_ref._cname),true)
- : oldFld.makeFrom(sig);
- Field ctorFld = ts==null ? null : ts.field("<ctor>");
- // A same-named struct shares its file's class object and flag.
- Field flag = ts==null ? null : ts.field("$init");
- ts = ctorFld==null
- ? TypeStruct.make(clzName,false,fld)
- : TypeStruct.make(clzName,false,fld,ctorFld);
- if( flag!=null ) ts=ts.add(flag);
- TypeFunPtr alloc = makeAllocator((TypeStruct)TYPES.get(fullName),ret,ctorFld);
- // Publish the source-callable allocator, not the private-memory helper.
- sig = alloc;
- ts = ts.replace(fld.makeFrom(alloc));
- ts = ts.add(Field.make("<new>",alloc,_code.alias(_ref._cname),true));
- TYPES.put(clzName,ts);
-
- // Insert a field in the containing class with the nested class type.
- // This is basically sugar for "val CLZNAME = <new>"
- // and is a nicer version of updateSelfAsFieldsDiscovered.
- _scope.define(typeName, sig, true, new FunPtrNode(sig,_code._start,_code.link(alloc).ret()).peephole(), loc());
-
- require("}");
- return require(_code.ZERO,";");
- }
-
- // Build the declaration-side allocation helper. Allocation sites only
- // call this function; all layout, initialization and publication choices
- // are made here, where the complete struct and constructor are known.
- private TypeFunPtr makeAllocator(TypeStruct ts, ReturnNode initRet, Field ctorFld) {
- TypeFunPtr ctor = ctorFld==null ? null : (TypeFunPtr)ctorFld._t;
- int nargs = ctor==null ? 0 : ctor.nargs()-2; // Hide self and selfMem
- Type[] atypes = new Type[nargs];
- for( int i=0; i<nargs; i++ ) atypes[i] = ctor.arg(i+2);
- TypeMemPtr ptr = TypeMemPtr.make(ts);
- TypeFunPtr sig = TypeFunPtr.make1((byte)2,true,atypes,ptr,_code.fidx(_ref._cname));
- String name = (ts._name+".<new>").intern();
-
- int oldUID = _code.UID();
- FunNode fun = (FunNode)new FunNode(loc(),sig,name,_ref,null,_ref._start).peephole();
- _code.link(fun);
- Node rpc = new ParmNode("$rpc",0,TypeRPC.BOT,fun,ConstantNode.seed(TypeRPC.BOT).peephole()).peephole();
- Node pub = new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun,
- new ProjNode(_ref._start,1,ScopeNode.MEM0).peephole()).peephole();
- Node[] parms = new Node[nargs];
- for( int i=0; i<nargs; i++ )
- parms[i] = new ParmNode("arg"+i,i+2,atypes[i],fun,ConstantNode.seed(atypes[i]).peephole()).peephole();
-
- NewNode nnn = new NewNode(ts,fun,off(ts," len")).init();
- Node self = new ProjNode(nnn,0,ts.str()).init();
- Node priv = new ProjNode(nnn,1,"#selfMem").init();
- Node[] initArgs = new Node[]{fun,pub,self,priv,
- new FunPtrNode(initRet.fun().sig(),_code._start,initRet).peephole()};
- CallEndNode cend = rawCall(initArgs,initRet.fun());
- Node ctl = new CProjNode(cend,0,ScopeNode.CTRL).peephole();
- pub = new ProjNode(cend,1,ScopeNode.MEM0).peephole();
- priv = new ProjNode(cend,2,"#selfMem").peephole();
-
- if( ctor != null ) {
- Node[] cargs = new Node[nargs+5];
- cargs[0]=ctl; cargs[1]=pub; cargs[2]=self; cargs[3]=priv;
- for( int i=0; i<nargs; i++ ) cargs[i+4]=parms[i];
- FunNode cfun = _code.link(ctor);
- cargs[cargs.length-1] = new FunPtrNode(ctor,_code._start,cfun.ret()).peephole();
- cend = rawCall(cargs,cfun);
- ctl = new CProjNode(cend,0,ScopeNode.CTRL).peephole();
- pub = new ProjNode(cend,1,ScopeNode.MEM0).peephole();
- priv = new ProjNode(cend,2,"#selfMem").peephole();
- }
-
- priv.keep();
- for( Field field : ts._fields ) {
- Type storage = field._t.makeStorage();
- Field escaped = field._final ? field : field.makeFrom(storage).makeFrom(true);
- pub.keep();
- Node esc = new EscapeNode(escaped,self,priv,pub).peephole();
- MemMergeNode merge = new MemMergeNode(null,pub);
- merge.alias(field._alias,esc);
- Node next = merge.peephole();
- pub.unkeep();
- pub = next;
- }
- priv.unkill();
- ReturnNode ret = (ReturnNode)new ReturnNode(ctl,pub,self,rpc,fun).peephole();
- fun.setRet(ret);
- fun._approxUIDs = _code.UID() - oldUID;
- _ref.addFun(_code,fun);
- return sig;
- }
-
- private CallEndNode rawCall(Node[] args, FunNode target) {
- // Establish this known declaration-side edge immediately, but do not
- // peephole or inline it while the surrounding factory is being built.
- // IterPeeps therefore starts with the complete initializer caller set.
- CallNode call = new CallNode(loc(),args).init();
- CallEndNode cend = new CallEndNode(call,TypeRPC.constant(_code.rpc(_ref._cname))).init();
- call.link(target);
- return cend;
- }
-
- // Parse a struct declaration (not an allocation); file-level is a class-init, and scope structs are normal
- private ReturnNode parseStruct( boolean isClz, String typeName ) {
- var oldExits = _ctorExits;
- _ctorExits = new ArrayList<>();
- // Record & restore global state set during parsing the <init> code
- Node oldCtrl= _scope.ctrl().keep();
- Node oldMem = _scope.mem ().keep();
-
- // Make the future clazz/instance struct.
- TypeStruct tself = (TypeStruct)TYPES.get(typeName);
- if( tself==null )
- TYPES.put(typeName, tself = TypeStruct.make(typeName,true));
- else {
- assert tself._open;
- TYPES.put(typeName,tself = tself.defined());
- }
-
- // Class object keeps a runtime flag to signal the <clinit> has run.
- if( isClz )
- TYPES.put(typeName,tself=tself.add(Field.make("$init",TypeInteger.BOOL,_code.alias(_ref._cname),false)));
-
- // Signature of the default constructor
- TypeFunPtr sig = structInitSig(isClz,tself);
- // Function and argument names of default constructor
- String fname = (typeName + (isClz ? ".<clinit>" : ".<init>")).intern();
- String[] ids = new String[]{"self", isClz ? "arg" : "#selfMem"};
-
- // Struct decls look like function bodies. Parse function body normally.
- ReturnNode ret = _parseFunctionBody(fname,sig,loc(),ids);
- _ctorExits = oldExits;
- // Unwind global state.
- ctrl(oldCtrl.unkeep());
- mem (oldMem .unkeep());
- return ret;
- }
-
- // <cl/init> signature: { self arg/selfMem -> BOT/selfMem }
- // Self-memory is the rare *private* (never aliased) memory for the new object.
- private TypeFunPtr structInitSig( boolean isClz, TypeStruct tself ) {
- // Private uninitialized memory from new object, no escaped values.
- TypeMem privMem = isClz ? null : TypeMem.make(1,tself,true,isClz,false,null,null);
- Type targ = isClz ? TypeInteger.BOT : privMem;
- // Return for private memory has to name all escaped fields, which are not known yet.
- // Escape ALL fields, and sharpen later.
- Type tret = isClz ? Type.BOTTOM : TypeMem.makePrivate(tself);
- Type[] targs = new Type[]{ TypeMemPtr.make((byte)2,tself,isClz), targ };
- // FIDX for FREFs is in the global namespace, needs the correct clz name.
- String clzBare = tself._name;
- int order = 3; // 3 is always the <init> order, 2nd in the file just behind the <clinit>
- if( isClz ) {
- assert startsClzPrefix( clzBare );
- clzBare = clzBare.substring(clzPrefix.length()).intern();
- order = 2; // 2 is always the <clinit> order, first in file and skipping reserved #1
- }
- return TypeFunPtr.make1((byte)2, true, targs, tret, _code.fidx(clzBare,order));
- }
-
- private static String initFieldName( String typeName ) {
- if( startsClzPrefix(typeName) )
- typeName = typeName.substring(clzPrefix.length());
- int idx = typeName.lastIndexOf('.');
- return (idx == -1 ? typeName : typeName.substring(idx+1)).intern();
- }
-
- private TypeStruct latestStruct( TypeStruct ts ) {
- Type t = TYPES.get(ts._name);
- return t instanceof TypeStruct ts2 ? ts2 : ts;
- }
-
- private Field fieldOrOpen( TypeStruct ts, String name, boolean xfinal ) {
- ts = latestStruct(ts);
- Field fld = ts.field(name);
- if( fld != null ) return fld;
- if( !ts._open || !ts._fref ) return null;
- fld = Field.make(name,Type.BOTTOM,_code.alias(_ref._cname),xfinal);
- TYPES.put(ts._name,ts = ts.add(fld));
- if( _ctorOpenStruct != null && _ctorOpenStruct._name == ts._name )
- _ctorOpenStruct = ts;
- return fld;
- }
-
- private Var defineOpenConstructorField( String name ) {
- if( _ctorOpenStruct==null ) return null;
- Field fld = fieldOrOpen(_ctorOpenStruct,name,true);
- if( fld == null ) return null;
- _scope.define(name,fld._t,fld._final,con(Type.TOP),loc());
- return _scope.lookup(name);
- }
-
-
- // Parse and return a type or null. Valid types always are followed by an
- // 'id' which the caller must parse. This lets us distinguish forward ref
- // types (which ARE valid here) from local vars in an (optional) forward
- // ref type position.
-
- // t = int|i8|i16|i32|i64|u8|u16|u32|u64|byte|bool | flt|f32|f64 | val | var | struct[?]
-
- // Structs have nested names, and can be discovered with a partial name
- // match. Example, searching for a type name "C.D" in the namespace "A.B".
- // This will match "A.B.C.D", then "A.C.D", then "C.D".
-
- // Will also match external names like "sys.io" or "externModule.subtype.subtype"
-
- private Type type() {
- // Only type with a leading `{` is a function pointer...
- if( peek('{') ) return typeFunPtr();
-
- // Otherwise you get a type name
- int old1 = pos();
- boolean writable = match("!");
- boolean readonly = !writable && match("~");
- String tname = _lexer.matchId();
- if( tname==null ) return posT(old1);
-
- // Convert the type name to a type.
- Type t0 = TYPES.get(tname), t1 = t0;
- // No new types as keywords
- if( KEYWORDS.contains(tname) ) {
- if( t0 == null ) return posT(old1);
- // Something like "int" or "f64" or "var".
- // Can be "int[]" so still need to check array-ness
- } else {
- if( t0 == Type.BOTTOM || t0 == Type.TOP ) return t0; // var/val type inference
-
- if( t0 != null ) {
- while( true ) {
- int old2 = pos();
- if( !match(".") ) break;
- String sname = matchId();
- if( sname==null ) { pos(old2); break; }
- sname = tname+"."+sname;
- Type t = TYPES.get(sname);
- if( t==null ) { pos(old2); break; }
- tname = sname;
- t0 = t;
- }
- } else {
- // Type A.B.C starts with just A. Search our nested type path,
- // e.g. path "X.Y.Z", in order:
-
- // If A==X its now a local type.
- // If another dot, repeat for B==Y then C==Z
- // Else if AX.B exists, done
- // Else AX.B is a local forward ref.
- //
- // Else its a short cut form, one of:
- // X.Y.Z.A
- // X.Y.A
- // X.A
- // Else this all failed, assume a forward ref with A as an external symbol:
- // A
-
- String root = _nestedType;
- int ridx = root.indexOf('.');
- if( ridx != -1 ) root = root.substring(0,ridx);
- if( tname.equals(root) ) {
- boolean localForward = false;
- while( true ) {
- int old2 = pos();
- if( !match(".") ) break;
- String sname = matchId();
- if( sname==null ) { pos(old2); break; }
- sname = tname+"."+sname;
- Type t = TYPES.get(sname);
- if( t==null && !(_nestedType.equals(tname) || _nestedType.startsWith(tname+".")) && !localForward ) {
- pos(old2);
- break;
- }
- tname = sname;
- t0 = t;
- localForward |= t==null;
- }
- } else {
- String nest = _nestedType, fullq = tname;
- while( true ) {
- fullq = nest+"."+tname; // Full qualified name
- t0 = TYPES.get(fullq);
- if( t0 != null )
- { tname = fullq; break; } // Search succeeded
- // Subtract a layer from nested typenames
- int idx = nest.lastIndexOf('.');
- if( idx== -1 ) break; // Search failed; leave as an external symbol
- nest = nest.substring(0,idx);
- }
- if( t0==null ) {
- CompUnit cu = ParseAll.findCompUnit(_code,_ref,tname);
- if( cu != null ) {
- tname = cu._cname;
- t0 = TYPES.get(tname);
- }
- while( true ) {
- int old2 = pos();
- if( !match(".") ) break;
- String sname = matchId();
- if( sname==null ) { pos(old2); break; }
- tname = tname+"."+sname;
- t0 = TYPES.get(tname);
- }
- } else {
- while( true ) {
- int old2 = pos();
- if( !match(".") ) break;
- String sname = matchId();
- if( sname==null ) { pos(old2); break; }
- sname = tname+"."+sname;
- Type t = TYPES.get(sname);
- if( t==null ) { pos(old2); break; }
- tname = sname;
- t0 = t;
- }
- }
- }
- }
-
- tname = tname.intern();
- // Still no type found? Assume forward reference
- t1 = t0 == null
- ? TypeStruct.forward(tname) // Null: assume a forward ref type
- : t0;
- }
-
- // Structs are always actually references
- if( t1 instanceof TypeStruct ts1 )
- t1 = TypeMemPtr.make(ts1);
-
- // A qualifier before the name applies to the struct reference, not
- // to an enclosing array. Arrays have their own [] / [~] qualifier.
- if( t1 instanceof TypeMemPtr tmp ) {
- if( readonly || !writable ) t1 = tmp.makeRO();
- } else if( writable || readonly )
- return posT(old1);
-
- // Nest arrays and '?' as needed
- Type t2 = t1;
- while( true ) {
- if( match("?") ) {
- if( t2 instanceof TypeMemPtr tmp ) {
- if( tmp.nullable() ) throw error("Type "+t2+" already allows null");
- t2 = tmp.makeNullable();
- } else
- throw error("Type "+t0+" cannot be null");
- } else if( match("[~]") ) {
- t2 = TypeMemPtr.make(typeAry(t2,true)).makeRO();
- } else if( match("[]") ) {
- t2 = TypeMemPtr.make(typeAry(t2,false));
- } else
- break;
- }
-
- // Check no forward ref
- if( t0 != null ) return t2;
- // Check valid forward ref, after parsing all the type extra bits.
- // Cannot check earlier, because cannot find required 'id' until after "[]?" syntax
- int old2 = pos();
- if( !match("!") ) match("~");
- String id = _lexer.matchId();
- if( !(peek(',') || peek(';') || peek('=') || match("->")) || id==null )
- return posT(old1); // Reset lexer to reparse
- pos(old2); // Reset lexer to reparse
- // Yes a forward ref, so declare it
- forwardRefType(tname,(TypeStruct)((TypeMemPtr)t1)._obj);
- // Return the (array, final) type
- return t2;
- }
-
- private TypeMemPtr forwardRefType( String tname, TypeStruct tself ) {
- TYPES.put(tname,tself);
- String clzName = addClzPrefix( tname );
- // Only a struct declaration publishes an instance initializer. A
- // forward type may name a file with just <clinit>, so do not invent
- // a constructor field (or overwrite an already parsed class).
- TYPES.putIfAbsent(clzName,TypeStruct.make(clzName,true));
- return TypeMemPtr.make(tself);
- }
-
- private TypeMemPtr forwardRefType( String tname ) {
- tname = fullTypeName(tname);
- return forwardRefType(tname,TypeStruct.forward(tname));
- }
-
- private String fullTypeName( String tname ) {
- String nest = _nestedType, fullq;
- while( true ) {
- fullq = nest+"."+tname;
- if( TYPES.get(fullq) != null )
- return fullq.intern();
- int idx = nest.lastIndexOf('.');
- if( idx== -1 ) return fullq.intern();
- nest = nest.substring(0,idx);
- }
- }
-
- // Make an array type of t. Always record a mutable version,
- // but return the requested version.
- private TypeStruct typeAry( Type t, boolean efinal ) {
- // Non-null reference arrays are valid types, even though an ordinary
- // zero-filled `new T[len]` cannot construct one. Allocation retains
- // that check in alloc(); trusted runtimes (notably argv construction)
- // may fill the private array completely before publishing it.
- String tname = ("[]"+t.str()).intern();
- TypeStruct ta = (TypeStruct)TYPES.get(tname);
- if( ta==null ) {
- // Primitive arrays use the null compunit
- String rcname = TYPES.get(t) instanceof TypeStruct ? _ref._cname : ("_."+t).intern();
- ta = TypeStruct.makeAry(tname,TypeInteger.U32,_code.alias(rcname),t,_code.alias(rcname), false );
- TYPES.put(tname,ta);
- }
- // Already have the aliases, just efinal is wrong
- return TypeStruct.makeAry(tname,TypeInteger.U32,ta._fields[0]._alias,t,ta._fields[1]._alias,efinal );
- }
-
- private Type implicitSelfType() {
- TypeStruct self = _scope.constructorSelf();
- // Ordinary instance methods accept the declared object shape. The
- // constructor-only receiver starts at the low/default field values,
- // and is built explicitly by constructorFunc().
- return self == null ? TypePtr.PTR : TypeMemPtr.make(self);
- }
-
- // A function type is `{ type... -> type }` or `{ type }`.
- private Type typeFunPtr() {
- int old = pos(); // Record lexer position
- match("{"); // Skip already-peeked '{'
- Type t0 = type(); // Either return or first arg
- if( t0==null ) return posT(old); // Not a function
- Ary<Type> ts = new Ary<>(Type.class);
- // Always reserve a receiver slot. Outside constructor scopes it is
- // broad and unused, so call sites can uniformly pass their self.
- ts.push(implicitSelfType());
- if( match("}") ) // No-arg function { -> type }
- return TypeFunPtr.make(match("?"),ts.asAry(),t0);
- ts.push(t0); // First argument
- while( true ) {
- if( match("->") ) { // End of arguments, parse return
- Type ret = type();
- if( ret==null || !match("}") )
- return posT(old); // Not a function
- return TypeFunPtr.make(match("?"),ts.asAry(),ret);
- }
- Type t1 = type();
- if( t1==null ) return posT(old); // Not a function
- ts.push(t1);
- }
- }
-
- // True if a TypeFunPtr, without advancing parser
- private boolean isTypeFun() {
- int old = pos();
- if( typeFunPtr()==null ) return false;
- pos(old);
- return true;
- }
-
- /**
- * Parse an expression of the form:
- *
- * <pre>
- * expr : bitwise [? expr [: expr]]
- * </pre>
- * @return an expression {@link Node}, never {@code null}
- */
- private Node parseExpression() {
- Node expr = parseLogical();
- return match("?") ? parseTrinary(expr,":") : expr;
- }
-
- /**
- * Parse an bitwise expression
- *
- * <pre>
- * bitwise : compareExpr (('&' | '|' | '^') compareExpr)*
- * </pre>
- * @return a bitwise expression {@link Node}, never {@code null}
- */
-
- private Node parseLogical() {
- Node lhs = parseBitwise();
- while (true) {
- if( false ) ;
- else if( match("&&") ) lhs = parseTrinary(lhs, "&&");
- else if( match("||") ) lhs = parseTrinary(lhs, "||");
- else break;
- }
- return lhs;
- }
-
- private Node parseBitwise() {
- Node lhs = parseEquality();
- while( true ) {
- if( false ) ;
- else if( matchOp('&') ) lhs = new AndNode(loc(),lhs,null);
- else if( matchOp('|') ) lhs = new OrNode(loc(),lhs,null);
- else if( match ("^") ) lhs = new XorNode(loc(),lhs,null);
- else break;
- lhs.setDef(2,parseEquality());
- lhs = peep(lhs);
- }
- return lhs;
- }
-
-
- /**
- * Parse an eq/ne expression
- */
- private Node parseEquality() {
- Node lhs = parseComparison();
- while( true ) {
- boolean eq = false;
- if( match("==") ) eq = true;
- else if( !match("!=") ) break;
- lhs.keep();
- Node rhs = parseComparison();
- lhs = peep(new BoolNode.EQ(lhs.unkeep(),rhs));
- if( !eq ) lhs = peep(new NotNode(lhs));
- }
- return lhs;
- }
-
- /**
- * Parse an expression of the form:
- *
- * <pre>
- * expr : shiftExpr < shiftExpr <= shiftExpr...
- * expr : shiftExpr > shiftExpr >= shiftExpr...
- * </pre>
- * @return an comparator expression {@link Node}, never {@code null}
- */
- private Node parseComparison() {
- Node lhs = parseShift();
- int dir = parseCompDir();
- // No compare
- if( dir==0 ) return lhs;
- lhs.keep();
-
- // Compare
- Node rhs = parseShift().keep();
- Node cmp = makeCompBool(dir,lhs.unkeep(),rhs).keep(); // Convert to a bool
-
- // Stacked compares?
- int dir0 = parseCompDir();
- if( dir0 == 0 ) {
- rhs.unkill();
- return cmp.unkeep();
- }
-
- // Keep the middle operand separately from the boolean result.
- // The next test compares adjacent operands, not that boolean.
- Node ifNode = new IfNode(ctrl(), cmp.unkeep()).peephole();
- Node ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep();
- Node ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole();
- // False side does nothing but capture memory & side-effects
- ScopeNode fail = _scope.dup();
- fail.ctrl(ifF);
-
- // Loop over stacked compares. Each loop moves the old RHS to become
- // the new LHS, so e.g. (a < b < c < d) compares (LHS:a < RHS:b) then
- // compares (LHS:b < RHS:c) then (LHS:c < RHS:d), etc.
- while( dir0 != 0 ) {
- if( Math.abs(dir) != Math.abs(dir0) )
- throw error("Mixing relational directions in a chained relational test");
- // True side parses next arm of test
- ctrl(ifT.unkeep());
- lhs = rhs; // Reuse the previous RHS exactly once
- rhs = parseShift().keep();
- cmp = makeCompBool(dir0,lhs.unkeep(),rhs); // Convert to a bool
- ifNode = new IfNode(ctrl(), cmp).peephole();
- ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep();
- ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole();
- // Merge result into the fail case
- ctrl(ifF);
- fail.mergeScopes(_scope.dup(), loc()).peephole();
- // Check next stacked compare
- dir0 = parseCompDir();
- }
- rhs.unkill();
- ctrl(ifT.unkeep());
- RegionNode r = fail.mergeScopes(_scope, loc()).init();
- _scope = fail;
- return new PhiNode("", r,con(0),con(1)).peephole();
- }
-
- private int parseCompDir() {
- if( match("<=") ) return -1; // Negative for "equals-or-less"
- if( match(">=") ) return -2; // 1 for <, 2 for >
- if( match("<" ) ) return 1; // Positive for just "less"
- if( match(">" ) ) return 2;
- return 0;
- }
-
- private Node makeCompBool( int dir, Node lhs, Node rhs ) {
- if( Math.abs(dir)==2 )
- { Node tmp=lhs; lhs=rhs; rhs=tmp; }
- Node cmp = dir < 0 ? new BoolNode.LE(lhs,rhs) : new BoolNode.LT(lhs,rhs);
- return peep(cmp);
- }
-
- /**
- * Parse an additive expression
- *
- * <pre>
- * shiftExpr : additiveExpr (('<<' | '>>' | '>>>') additiveExpr)*
- * </pre>
- * @return a shift expression {@link Node}, never {@code null}
- */
- private Node parseShift() {
- Node lhs = parseAddition();
- while( true ) {
- if( false ) ;
- else if( match("<<") ) lhs = new ShlNode(loc(),lhs,null);
- else if( match(">>>")) lhs = new ShrNode(loc(),lhs,null);
- else if( match(">>") ) lhs = new SarNode(loc(),lhs,null);
- else break;
- lhs.setDef(2,parseAddition());
- ParseException err;
- if( (err=lhs.err()) != null )
- throw err;
- lhs = peep(lhs);
- }
- return lhs;
- }
-
- /**
- * Parse an additive expression
- *
- * <pre>
- * additiveExpr : multiplicativeExpr (('+' | '-') multiplicativeExpr)*
- * </pre>
- * @return an add expression {@link Node}, never {@code null}
- */
- private Node parseAddition() {
- Node lhs = parseMultiplication();
- while( true ) {
- if( false ) ;
- else if( match("+") ) lhs = new AddNode(lhs,null);
- else if( match("-") ) lhs = new SubNode(lhs,null);
- else break;
- lhs.setDef(2,parseMultiplication());
- lhs = peep(lhs);
- }
- return lhs;
- }
-
- /**
- * Parse an multiplicativeExpr expression
- *
- * <pre>
- * multiplicativeExpr : unaryExpr (('*' | '/') unaryExpr)*
- * </pre>
- * @return a multiply expression {@link Node}, never {@code null}
- */
- private Node parseMultiplication() {
- var lhs = parseUnary();
- while( true ) {
- if( false ) ;
- else if( match("*") ) lhs = new MulNode(lhs,null);
- else if( match("/") ) lhs = new DivNode(lhs,null);
- else break;
- lhs.setDef(2,parseUnary());
- lhs = peep(lhs);
- }
- return lhs;
- }
-
- /**
- * Parse a unary minus expression.
- *
- * <pre>
- * unaryExpr : ('-') unaryExpr | '!') unaryExpr | postfixExpr | primaryExpr | '--' Id | '++' Id
- * </pre>
- * @return a unary expression {@link Node}, never {@code null}
- */
- private Node parseUnary() {
- // Pre-dec/pre-inc
- int old = pos();
- if( match("--") || match("++") ) {
- int delta = _lexer.peek(-1)=='+' ? 1 : -1; // Inc vs Dec
- int start = pos();
- String name = _lexer.matchId();
- Var var = name==null ? null : _scope.lookup(name);
- if( var!=null && !(var.type() instanceof TypeMemPtr) ) {
- if( var._extern==0 && var._final && !_scope.canInit(var) )
- throw error("Cannot reassign final '"+var._name+"'");
- // Resolve fields just as a read or postfix update does. Updating
- // their declaration's local slot would silently lose the Store.
- pos(start);
- return parsePrimary(delta);
- }
- // Reset, try again
- pos(old);
- }
- if (match("-")) return peep(new MinusNode(parseUnary()));
- if (match("!")) return peep(new NotNode(parseUnary()));
- // Else be a primary expression
- return parsePrimary();
- }
-
- /**
- * Parse a primary expression:
- *
- * <pre>
- * primaryExpr : integerLiteral | "string" | 'char' | true | false | null |
- * new Type | '(' expression ')' | Id['++','--'] |
- * type['.' Id]*
- * </pre>
- * @return a primary {@link Node}, never {@code null}
- */
- private Node parsePrimary() { return parsePrimary(0); }
-
- // pre is +/-1 for a prefix update of a known name, zero otherwise.
- private Node parsePrimary(int pre) {
- if( _lexer.isNumber(_lexer.peek()) ) return parseLiteral();
- if( _lexer.peek('"') ) return parsePostfix(newString(parseString()));
- if( matchx("true" ) ) return con(1);
- if( matchx("false") ) return _code.ZERO;
- if( matchx("null" ) ) return con(Type.NIL);
- if( match ("'" ) ) return parseChar();
- if( match ("(" ) ) return parsePostfix(require(parseAsgn(), ")"));
- if( matchx("new" ) ) return parsePostfix(alloc());
- if( match ("{" ) ) return parsePostfix(require(func(),"}"));
-
- int pos = pos();
- // Expect an identifier now
- String id = _lexer.matchId();
- if( id == null || KEYWORDS.contains(id) )
- throw errorSyntax("an identifier or expression");
- int pos2 = pos();
- // Attempt a local var lookup first
- Var var = _scope.lookup(id);
- if( var==null ) {
- // Not a local var, try again as a type name
- pos(pos);
- Type t = type();
- if( t!=null ) {
- // Static <clinit> typename lookup
- if( peek('.') ) // type.fld, which is really a field lookup in the class object
- return parsePostfix(con(t));
- // Direct reference to a <clinit> type. Convert the instance
- // type to a class type name
- if( !(t instanceof TypeMemPtr tmp) ) {
- //return new FRefNode(addClzPrefix(tmp._obj._name), loc()).peephole();
- // Not a local var, but yes a bare type... disallow a local
- // FREF and return null, not-a-primary parse.
- pos(pos);
- return null;
- }
- }
-
- // Insert FREF outside enclosing Kind.Func scope.
- pos(pos2);
- t = FRefNode.FREF_TYPE;
- // Check if this is a class.
- CompUnit cu = ParseAll.findCompUnit(_code,_ref,id);
- if( cu!=null ) {
- // Must be a final class-type
- String clzName = addClzPrefix(cu._cname);
- TypeStruct clz = (TypeStruct)TYPES.get(clzName);
- if( clz == null )
- TYPES.put(clzName,clz = TypeStruct.make(clzName,true));
- t = TypeMemPtr.make((byte)2,clz,true);
- }
- // Define the FRef
- var = _scope.defineFRef(id,t,true,loc());
- _frefs.push((FRefNode)_scope.in(var));
- }
-
- // Load local value
- Node rvalue = _scope.in(var);
- if( var._extern!=0 )
- return parsePostfixName(rvalue,id,pre);
- if( var._fref )
- return parsePostfix(rvalue);
- // A constructor may read the unfinished object, but each field it
- // reads must already be initialized on every path. Read the current
- // local value; other fields may still be waiting for their first write.
- if( inExplicitConstructor() && var._uninit && missingInit(rvalue,new BitSet()) )
- throw error("Field '"+id+"' might not be initialized",var._loc);
-
- // Check for an instance field load
- int kx = _scope.kindx(var); // Declaration scope
- int fx = _scope.enclosingFunction(); // Enclosing function scope
- Kind kk = _scope._kinds.at(kx);
- Kind fk = _scope._kinds.at(fx);
- // Access instance field from 'self'
- if( kx+1 == fx && kk instanceof Kind.Func inst && FunNode.isInstance(inst._name) && fk instanceof Kind.Func method && !inExplicitConstructor() )
- return parsePostfixName(_scope.in(method._lexSize),id,pre);
-
- // Check for a function-escaping variable; these require true
- // closures. Final constants are OK; final vars require a hidden var
- // argument; not-final fields can just use an explicit struct arg.
- if( kx < fx && !inExplicitConstructor() && // Out of scope
- !(var._final && rvalue._type.isConstant()) ) { // Not a final constant
- if( kx > 0 ) // Global scope is OK, only ever one of these (not one per function invoke)
- throw error("Variable '"+var._name+"' is out of function scope and must be a final constant");
-
- // var.idx==2 is self argument, 3 is args, 4 or more are now static globals
- if( var._idx >= 4 ) {
- // Read from global scope.
- // Surely a nicer way to get the top-level scope compile name?
- Var clzptr = _scope.var( 2 );
- // Get the most recent sharpen global
- TypeStruct clz = (TypeStruct) TYPES.get( ((TypeMemPtr) clzptr.type())._obj._name );
- TypeMemPtr clztmp = TypeMemPtr.make( (byte) 2, clz, true );
- return parsePostfixName( con( clztmp ), id,pre );
- //return parsePostfixName(_scope.in(2),id);
- }
- }
-
- // Check for assign-update, x += e0;
- char ch = pre==0 ? _lexer.matchOperAssign() : (char)pre;
- if( ch==0 ) { // Normal primary, check for postfix updates
- int selfx = _scope.kindFcn(var)._lexSize;
- // Might be a method call from inside a method, so no explicit "self".
- // Pass the in-scope "self"
- return parsePostfixMethod(rvalue,_scope.in(_scope.update(_scope.var(selfx),null)));
- }
- // Assign-update direct into Scope
- if( var._final && !_scope.canInit(var) )
- throw error("Cannot reassign final '"+var._name+"'");
- checkSharedFunction(var);
- Node op = opAssign(ch,rvalue, var.type() );
- _scope.update(var,op);
- if( postfix(ch) ) rvalue.unkeep();
- return pre==0 && postfix(ch) ? rvalue : op;
- }
-
- // Check for assign-update, "x += e0".
- // Returns "x + e0"; the caller assigns value.
- // if ch is postfix, then lhs is kept and caller will unkeep and return it.
- private Node opAssign( char ch, Node lhs, Type t ) {
- // RHS of the update.
- lhs.keep(); // Alive across parseAsgn
- Node rhs =
- (byte)ch == 1 ? con( 1) : // var++
- (byte)ch == -1 ? con(-1) : // var--
- parseAsgn(); // var op= rhs
- if( !postfix(ch) ) lhs.unkeep(); // Allow to die in next peep
- // 4 cases:
- // int + int ==>> narrow int
- // int + flt ==>> error, caller must fail assigning flt into int
- // flt + int ==>> use float op, wrap toFloat()
- // flt + flt ==>> use float op
- Node op = switch(ch) {
- case 1, (char)-1,
- '+' -> new AddNode(lhs,rhs);
- case '-' -> new SubNode(lhs,rhs);
- case '*' -> new MulNode(lhs,rhs);
- case '/' -> new DivNode(lhs,rhs);
- case '|' -> new OrNode(null,lhs,rhs);
- case '&' -> new AndNode(null,lhs,rhs);
- case '^' -> new XorNode(loc(),lhs,rhs);
- case Lexer.SHL -> new ShlNode(loc(),lhs,rhs);
- case Lexer.SAR -> new SarNode(loc(),lhs,rhs);
- case Lexer.SHR -> new ShrNode(loc(),lhs,rhs);
- default -> throw TODO();
- };
- // Convert to float ops, or narrow int types; error if not declared type.
- // Also, if postfix LHS is still keep()
- return convertExpr(peep(op),t);
- }
-
-
- /**
- Parse an allocation
- */
- private Node alloc() {
- Type t = type();
- if( t==null ) {
- String tname = _lexer.matchId();
- if( tname==null || KEYWORDS.contains(tname) )
- throw error("Expected a type");
- t = forwardRefType(tname);
- }
- // Parse ary[ length_expr ]
- if( match("[") ) {
- if( !t.makeZero().isa(t) )
- throw error("Cannot allocate a non-nullable, since arrays are always zero/null filled");
- Node len = parseAsgn().keep();
- require("]");
- return allocArray(typeAry(t,false),len);
- } else {
- if( !(t instanceof TypeMemPtr tmp) )
- throw error("Cannot allocate a "+t.str());
- return allocStruct(tmp._obj);
- }
- }
-
- private Node allocStruct(TypeStruct ts) {
- Ary<Node> ctorArgs = constructorArgs();
- // A field declaration can leave a short forward type name in TYPES.
- // Allocation happens in lexical context, so resolve that short name
- // to the completed nested type before selecting its class helper.
- String typeName = ts._name.indexOf('.') == -1 ? fullTypeName(ts._name) : ts._name;
- Type resolved = TYPES.get(typeName);
- if( resolved instanceof TypeStruct rts ) ts = rts;
- TypeStruct clz = (TypeStruct)TYPES.get(addClzPrefix(typeName));
- if( clz==null )
- throw error("Unknown struct type '" + ts._name + "'");
- Field afld = clz.field("<new>");
- Node alloc;
- if( afld != null ) {
- TypeFunPtr tfp = (TypeFunPtr)afld._t;
- FunNode fun = _code.link(tfp);
- alloc = fun==null
- ? new FRefNode((addClzPrefix(typeName)+".<new>").intern(),loc())
- : new FunPtrNode(tfp,_code._start,fun.ret()).peephole();
- } else {
- if( !ts._fref )
- throw error("Constructor arguments for '" + ts._name + "' but no constructor is defined");
- Type[] sig = new Type[ctorArgs._len];
- for( int i=0; i<ctorArgs._len; i++ )
- sig[i] = ctorArgs.at(i)._type;
- alloc = new FRefNode((addClzPrefix(typeName)+".<new>").intern(),loc(),
- TypeFunPtr.make(false,sig,TypeMemPtr.make(ts)));
- }
- Ary<Node> args = new Ary<>(Node.class);
- args.add(ctrl()); args.add(mem());
- for( Node arg : ctorArgs ) args.add(arg.unkeep());
- args.add(alloc);
- return functionCall(args);
- }
-
- private Ary<Node> constructorArgs() {
- Ary<Node> args = new Ary<>(Node.class);
- if( !match("(") )
- return args;
- while( !peek(')') ) {
- Node arg = parseAsgn();
- if( arg==null ) break;
- args.push(arg.keep());
- if( !match(",") ) break;
- }
- require(")");
- return args;
- }
-
- private Node allocArray(TypeStruct ts, Node len) {
- Field lenFld = ts.field("#" );
- Field bodyFld = ts.field("[]");
-
- ConFldOffNode base = off(ts, "[]");
- int scale = ts.aryScale();
- Node size = peep(new AddNode(base,peep(new ShlNode(null,len,con(scale)))));
-
- // Build a NewNode; takes in and puts out all aliases.
- NewNode nnn = new NewNode(ts, ctrl(), size ).init();
- ProjNode self = new ProjNode(nnn,0,ts.str()).init().keep();
- ProjNode smem = new ProjNode(nnn,1,"#selfMem").init().keep();
-
- // Store length. Rest of array is zero'd via CALLOC during CodeGen.
- // Length is casted to sanity.
- // TODO: Needs runtime check.
- Node st = peep(new StoreNode(null, "#", 1, TypeInteger.U32, null, smem, self, off(ts,"#"), len.unkeep(), true));
-
- // TODO: Allow add-on "constructor" to init the array elements
-
- Node prior = mem();
- Node esc = peep(new EscapeNode(lenFld,self,st,prior));
- mem(mergeAlias(prior,lenFld._alias,esc));
- prior = mem();
- esc = peep(new EscapeNode(bodyFld,self,smem,prior));
- mem(mergeAlias(prior,bodyFld._alias,esc));
- smem.unkeep();
- return self.unkeep();
- }
-
- private Node newString(String s) {
- TypeStruct ts = typeAry(TypeInteger.U8,true);
- int lenAlias = ts.field("#" )._alias;
- int elemAlias = ts.field("[]")._alias;
- TypeConAryB body = TypeConAryB.make(s);
- TypeInteger slen = TypeInteger.constant(s.length());
- // Make a TMP, not-null (byte)2, singleton (true) (requires text
- // strings are hash-interned), with a TypeStruct having a constant
- // array body.
- TypeMemPtr str = TypeMemPtr.make((byte)2,TypeStruct.makeAry("[]u8", slen, lenAlias, body, elemAlias, true),true);
- assert str.isConstant();
- return con(str.makeRO());
- }
-
- /**
- * Parse postfix expression; this can be a field expression, an array
- * lookup or a postfix operator like '#'
- *
- * <pre>
- * expr ('.' FIELD)* [ = expr ] // Field reference
- * expr '#' // Postfix unary read operator
- * expr ['++' | '--' ] // Postfix unary write operator
- * expr ('[' expr ']')* = [ = expr ] // Array reference
- * expr '(' [args,]* ')' // Function call
- * </pre>
- */
- private Node parsePostfixMethod(Node expr, Node self) {
- // TODO: This is too broad because the parser lacks strong types here.
- // Some normal functions can end up with a "self" argument.
-
- // Self method call? Self is a TMP-not-CLZ, then assume and pass self.
- if( match("(") ) {
- expr = parsePostfix(require(functionCall(expr,self),")"));
- } else if( expr != self )
- self.isKill(); // Not a method call, no need for self
- return parsePostfix(expr);
- }
- private Node parsePostfix(Node expr) {
- _lexer.skipWhiteSpace();
- Lexer site = loc();
- String name;
- if( match(".") ) name = requireId();
- else if( match("#") ) name = "#";
- else if( match("[") ) name = "[]";
- // Can get here without an explicit receiver: "f()" or
- // "self.method()(next)(next)(next)". Pass the current function self.
- else if( match("(") ) return parsePostfix(require(functionCall(expr,defaultSelf()),")"));
- else return expr; // No postfix
-
- return parsePostfixName(expr,name,0,site);
- }
-
- private boolean privateSelf(Node ptr) {
- while( ptr instanceof ConvertNode || ptr instanceof ReadOnlyNode || ptr instanceof GuardNode ) ptr=ptr.in(1);
- Node self=defaultSelf();
- while( self instanceof ConvertNode || self instanceof ReadOnlyNode || self instanceof GuardNode ) self=self.in(1);
- return ptr==self;
- }
-
- // An explicit self.x read sees the same current value as bare x. Loading
- // the allocation here would miss updates still held in local SSA values.
- private Node privateField(String name, int pre) {
- int idx=fieldVarIdxBefore(_scope._kinds.at(_scope.enclosingFuncOrDecl())._lexSize,name);
- if( idx<0 ) throw error("Cannot resolve field '"+name+"'");
- Var v=_scope.var(idx);
- if( pre==0 && matchOpx('=','=') ) {
- checkSharedFunction(v);
- Node value=parseAsgn().keep();
- _scope.update(v,convertExpr(value,v.type()));
- return value.unkeep();
- }
- Node value=_scope.in(_scope.update(v,null));
- if( v._uninit && missingInit(value,new BitSet()) )
- throw error("Field '"+name+"' might not be initialized",v._loc);
- char ch=pre==0 ? _lexer.matchOperAssign() : (char)pre;
- if( ch!=0 ) {
- checkSharedFunction(v);
- Node op=opAssign(ch,value,v.type());
- _scope.update(v,op);
- if( pre!=0 && postfix(ch) ) value.unkeep();
- return pre==0 && postfix(ch) ? value.unkeep() : op;
- }
- return parsePostfixMethod(value,defaultSelf());
- }
-
- private void checkSharedFunction(Var v) {
- if( _scope.canInit(v) && v._final && v.type() instanceof TypeFunPtr t && t.isConstant() )
- throw error("Cannot reassign shared function '"+v._name+"'");
- }
-
- // A loop Phi is temporarily bottom before its backedge is parsed. That is
- // not an unwritten field: follow its live inputs to the actual defaults.
- private boolean missingInit(Node value, BitSet seen) {
- if( value==null || seen.get(value._nid) ) return false;
- seen.set(value._nid);
- if( value instanceof PhiNode phi && !(value instanceof ParmNode) ) {
- for( int i=1; i<phi.nIns(); i++ )
- if( phi.region().in(i)!=null && phi.region().in(i)._type!=Type.XCONTROL && missingInit(phi.in(i),seen) )
- return true;
- return false;
- }
- return !initialized(value._type);
- }
-
- private Node defaultSelf() {
- int selfx = _scope._kinds.at(_scope.enclosingFuncOrDecl())._lexSize;
- return _scope.in(_scope.update(_scope.var(selfx),null));
- }
-
- /**
- * Parse postfix expression; this can be a field expression, an array
- * lookup or a postfix operator like '#'
- *
- * <pre>
- * expr ['++' | '--' ] // Postfix unary write operator
- * expr [ [op]= expr ] // Field reference
- * expr ('[' expr ']')* [ [op]= expr ] // Array reference
- * </pre>
- */
-
- // pre is +/-1 for a prefix update, otherwise parse a postfix operation.
- private Node parsePostfixName(Node expr, String name, int pre) {
- return parsePostfixName(expr,name,pre,loc());
- }
-
- private Node parsePostfixName(Node expr, String name, int pre, Lexer site) {
- if( inExplicitConstructor() && privateSelf(expr) ) return privateField(name,pre);
- // Keep expr across possible updates
- expr.keep();
-
- // Get the best known TypeStruct from a pointer type.
- TypeStruct ts = null, ts2;
- if( expr._type instanceof TypeMemPtr tmp && (ts2=((TypeStruct)TYPES.get(tmp._obj._name))) != null )
- ts = (TypeStruct)tmp._obj.join(ts2);
- // A Load's flow type is intentionally not promised during parsing,
- // but its declaring field type is still known. Recover that static
- // type for chained accesses such as `_grow(1).buf[len++]`; otherwise
- // the array Store is parsed as alias #1 and can hide the independent
- // side effect of the postfix `len++`.
- if( ts==null && expr instanceof LoadNode load ) {
- TypeStruct ots = load.off() instanceof ConFldOffNode coff ? coff._ts : null;
- if( ots==null && load.ptr()._type instanceof TypeMemPtr owner ) {
- Type owner0 = TYPES.get(owner._obj._name);
- ots = owner0 instanceof TypeStruct obj ? obj : owner._obj;
- }
- if( ots != null ) {
- Field loaded = ots.field(load._name);
- if( loaded != null && loaded._t instanceof TypeMemPtr ltmp ) {
- Type loaded0 = TYPES.get(ltmp._obj._name);
- ts = loaded0 instanceof TypeStruct obj ? obj : ltmp._obj;
- }
- }
- }
-
- Field fld = ts==null ? null : ts.field(name);
- if( fld==null && ts!=null && !startsClzPrefix(ts._name) ) {
- Type clz0 = TYPES.get(addClzPrefix(ts._name));
- if( clz0 instanceof TypeStruct clz && (fld = latestStruct(clz).field(name)) != null ) {
- expr.unkeep();
- ts = latestStruct(clz);
- expr = con(TypeMemPtr.make((byte)2,ts,true)).keep();
- }
- }
- if( fld==null && ts!=null )
- fld = fieldOrOpen(ts,name,false);
- if( fld==null && ts!=null && !latestStruct(ts)._open )
- throw error("Accessing unknown field '"+name+"' from '*"+ts._name+"'");
-
- // We have to record "active touches" of a new class, to track when we
- // need to force class initialization. Basically, we insert an CLInit,
- // and if it does not fold away it'll turn into a runtime check.
- Kind.Func func = (Kind.Func)_scope._kinds.at(_scope.enclosingFuncOrDecl());
- if( (ts==null || ts==TypeStruct.BOT || ts.isHigh() || startsClzPrefix(ts._name)) &&
- !(ts!=null && (ts._name+".<clinit>").equals(func._name)) )
- classInit(site,expr);
-
- // With no field, we will update the bulk memory
- int alias = fld==null ? 1 : fld._alias;
- Type decl = fld==null ? Type.BOTTOM : fld._t;
-
- // Field offset; fixed for structs, computed for arrays
- Node off = (name=="[]"
- // Array element math, with unknown base offset and shift amount.
- ? peep(new AddNode(fldoff(expr,"[]"),peep(new ShlNode(null,require(parseAsgn(),"]"),fldoff(expr,"<<")))))
- // Named field offset on an unknown struct type
- : fldoff(expr,name)).keep();
-
- // Disambiguate "obj.fld==x" boolean test from "obj.fld=x" field assignment
- if( pre==0 && matchOpx('=','=') ) {
- // Field assignment
- Node val = parseAsgn().keep();
- // Lift value for store
- Node lift = fld==null ? val : new ConvertNode(fld._t,val).peephole();
- // Memory for store, post assignment expression
- Node mem = mem();
- // Store to field
- mem.keep();
- if( expr._type instanceof TypeMemPtr view && view._ro && ctrl()._type!=Type.XCONTROL )
- throw error("Cannot modify final field '"+name+"'");
- Node st = new StoreNode(loc(), name, alias, decl, ctrl(), mem, expr.unkeep(), off.unkeep(), lift, false).peephole().keep();
- storeMem(st,alias,mem);
- st.unkeep();
- mem.unkeep();
-
- return val.unkeep(); // "obj.a = expr" returns the expression while updating memory
- }
-
- // Memory for load
- Node mem = mem();
- // Loading from a constant array, the declared type is the
- // meet-over-elements and not the array itself.
- if( name=="[]" && decl instanceof TypeConAry conary )
- decl = conary.elem();
-
- // Load field
- Node load = peep(new LoadNode(loc(),name, alias, ctrl(), mem, expr, off));
-
- // Check for assign-update, "ptr.fld += expr" or "ary[idx]++"
- char ch = pre==0 ? _lexer.matchOperAssign() : (char)pre;
- if( ch!=0 ) {
- if( fld!=null && decl==Type.BOTTOM )
- throw error( "'" + ts._name + "' is not fully initialized, field '" + fld._fname + "' needs to be set in a constructor" );
- Node op = opAssign(ch,load, decl ).keep();
- mem.keep();
- if( expr._type instanceof TypeMemPtr view && view._ro && ctrl()._type!=Type.XCONTROL )
- throw error("Cannot modify final field '"+name+"'");
- Node st = new StoreNode(loc(), name, alias, decl, ctrl(), mem, expr.unkeep(), off.unkeep(), op, false).peephole().keep();
- storeMem(st,alias,mem);
- st.unkeep();
- mem.unkeep();
-
- op.unkeep(); // A narrow Store may have folded away the expression's conversion.
- if( postfix(ch) ) load.unkeep();
- return pre==0 && postfix(ch) ? load : op;
- }
- off.unkill();
- // Might be a method call, so pass expr as 'self'
- return parsePostfixMethod(load,expr.unkeep());
- }
-
-
- // zero/sign extend. "i" is limited to either classic unsigned (min==0) or
- // classic signed (min=minus-power-of-2); max=power-of-2-minus-1.
- private Node zsMask(Node val, Type t ) {
- if( val._type instanceof TypeFloat && t instanceof TypeInteger )
- val = new ToIntegerNode(val).peephole();
- if( !(val._type instanceof TypeInteger tval && t instanceof TypeInteger t0 && !tval.isa(t0)) ) {
- if( !(val._type instanceof TypeFloat tval && t instanceof TypeFloat t0 && !tval.isa(t0)) )
- return val;
- // Float rounding
- return peep(new RoundF32Node(val));
- }
- if( t0._min==0 ) // Unsigned
- return peep(new AndNode(null,val,con(t0._max)));
- // Signed extension
- int shift = Long.numberOfLeadingZeros(t0._max)-1;
- Node shf = con(shift);
- if( shf._type==TypeInteger.ZERO )
- return val;
- return peep(new SarNode(null,peep(new ShlNode(null,val,shf.keep())),shf.unkeep()));
- }
-
- /**
- * Parse a function body; the caller will parse the surrounding "{}"
- *
- * <pre>
- * { [type arg,]* -> expr }
- * { expr } // The no-argument function
- * </pre>
- */
- private Node func() {
- Ary<Type> ts = new Ary<>(Type.class);
- Ary<String> ids = new Ary<>(String.class);
- java.util.BitSet finals = new java.util.BitSet();
- Type self = implicitSelfType();
- ts.push(self);
- ids.push(self == TypePtr.PTR ? "#self" : "self");
-
- // Parse other arguments
- _lexer.skipWhiteSpace();
- Lexer loc = loc(); // First argument location
- while( true ) {
- Type t = type(); // Arg type
- if( t==null ) break;
- finals.set(ids.size(),bindingFinal(t));
- String id = requireId();
- ts .push(t ); // Push type/arg pairs
- ids.push(id);
- match(",");
- }
- require("->");
- // Make a concrete function type, with a fidx
- TypeFunPtr tfp = TypeFunPtr.make1((byte)2,true,ts.asAry(),Type.BOTTOM,_code.fidx(_ref._cname));
- ReturnNode ret = parseFunctionBody(tfp,loc,finals,ids.asAry());
- return new FunPtrNode(tfp,_code._start,ret).peephole();
- }
-
- private Node constructorFunc(String name) {
- require("{");
- Ary<Type> ts = new Ary<>(Type.class);
- Ary<String> ids = new Ary<>(String.class);
- java.util.BitSet finals = new java.util.BitSet();
- TypeStruct self = _scope.constructorSelf();
- assert self != null;
- ts.push(TypeMemPtr.make(constructorRecv(self)));
- ids.push("self");
- // Explicit constructors use the same hidden allocation arguments as
- // <init>: public memory is the call memory edge (index 1), followed by
- // self (index 2) and the private, unescaped object memory (index 3).
- ts.push(TypeMem.makePrivate(self));
- ids.push("#selfMem");
-
- _lexer.skipWhiteSpace();
- Lexer loc = loc();
- while( true ) {
- Type t = type();
- if( t==null ) break;
- finals.set(ids.size(),bindingFinal(t));
- String id = requireId();
- ts .push(t );
- ids.push(id);
- match(",");
- }
- require("->");
- TypeFunPtr tfp = TypeFunPtr.make1((byte)2,true,ts.asAry(),TypeMem.makePrivate(self),_code.fidx(_ref._cname));
- Node[] preParse = _scope.save();
- ReturnNode ret = _parseFunctionBody((self._name+"."+name).intern(),tfp,loc,finals,ids.asAry());
- _scope.restore(preParse);
- require("}");
- return new FunPtrNode(tfp,_code._start,ret).peephole();
- }
-
- /**
- * Parse function call arguments; caller will parse the surrounding "()"
- * <pre>
- * ( arg* )
- * </pre>
- */
- private Node functionCall(Node fcn, Node self) {
- Lexer site = loc();
- site._position--; // Opening '('; arguments can contain nested calls.
- if( fcn._type == Type.NIL )
- throw error("Calling a null function pointer");
- fcn.keep(); // Keep while parsing args
-
- Ary<Node> args = new Ary<>( Node.class );
- args.push(null); // Space for ctrl,mem
- args.push(null);
- if( self != null ) // Method call has a self
- args.push(self.keep());
- while( !peek(')') ) {
- Node arg = parseAsgn();
- if( arg==null ) break;
- args.push(arg.keep());
- if( !match(",") ) break;
- }
- // Control & memory after parsing args
- args.set(0,ctrl().keep());
- args.set(1,mem().keep());
- args.push(fcn); // Function pointer
- // Unkeep them all
- for( Node arg : args )
- arg.unkeep();
- return functionCall(args,site);
- }
-
- private Node functionCall( Ary<Node> args ) {
- return functionCall(args,loc());
- }
-
- private Node functionCall( Ary<Node> args, Lexer site ) {
- // Dead into the call? Skip all the node gen
- if( ctrl()._type == Type.XCONTROL ) {
- for( Node arg : args )
- if( arg.isUnused() )
- arg.kill();
- return con(Type.TOP);
- }
-
- // Into the call
- CallNode call = (CallNode)new CallNode(site, args.asAry()).peephole();
- // Post-call setup
- CallEndNode cend = (CallEndNode)new CallEndNode(call,TypeRPC.constant(_code.rpc(_ref._cname))).peephole();
- call.peephole(); // Rerun peeps after CallEnd, allows early inlining
- cend = (CallEndNode)cend.keep().peephole(); // TODO: Might inline
- // Control from CallEnd
- ctrl(new CProjNode(cend,0,ScopeNode.CTRL).peephole());
- // Memory from CallEnd
- Node mem = new ProjNode(cend,1,ScopeNode.MEM0).peephole();
- mem(mem);
- // Call result
- return new ProjNode(cend.unkeep(),2,"#2").peephole();
- }
-
- // Just after parsing "type foo = " can parse `"C"`
- private boolean isExternDecl( ) {
- int old = pos();
- String s = parseString();
- if( "C".equals(s) ) return true;
- pos(old);
- return false;
- }
- // External linked constant
- private ConstantNode externDecl( String ex, Type t ) {
- if( t instanceof TypeFunPtr tfp ) { // Generic TFP from type parse
- int fidx = _code.fidx(_ref._cname); // Get a FIDX
- tfp = tfp.makeFrom(fidx); // Become a constant
- _code.externFunc(fidx,ex); // Map fidx to extern name
- t = tfp;
- }
- return (ExternNode)(new ExternNode(t,ex).peephole());
- }
-
- // A lexical C binding has a singleton namespace with one external field.
- // The namespace occupies no storage; its field offset supplies the address.
- private Node externData(String name, Type t) {
- if( !(t instanceof TypeInteger) && !(t instanceof TypeFloat) )
- throw error("C data binding requires an integer or float type");
- String cname = ("C:"+name).intern();
- TypeStruct ts = TypeStruct.make(cname,false,Field.make(name,t,_code.externAlias(name),false,true));
- Type old = TYPES.putIfAbsent(cname,ts);
- if( old!=null && old!=ts )
- throw error("Conflicting types for C data '"+name+"'");
- return con(TypeMemPtr.make((byte)2,ts,true));
- }
-
- /**
- * Parse integer literal
- *
- * <pre>
- * integerLiteral: [1-9][0-9]* | [0]
- * floatLiteral: [digits].[digits]?[e [digits]]?[f]?
- * </pre>
- */
- private ConstantNode parseLiteral() { return con(_lexer.parseNumber()); }
- ConstantNode con( long con ) { return _code.con(con); }
- ConstantNode con( Type t ) { return _code.con(t ); }
- private void classInit(Lexer site, Node ptr) {
- // Ordinary load machinery resolves the flag alias and offset when
- // the receiver is still a forward reference.
- Field flag = ptr._type instanceof TypeMemPtr tmp ? tmp._obj.field("$init") : null;
- int alias = flag==null ? 1 : flag._alias;
- Node ready = new LoadNode(site,"$init",alias,null,mem(),ptr,fldoff(ptr,"$init")).peephole();
- CLInitNode init = new CLInitNode(site,ctrl(),mem(),ptr,ready).peephole().keep();
- ctrl(new CProjNode(init,0,ScopeNode.CTRL).peephole());
- Node bulk = new ProjNode(init,1,ScopeNode.MEM0).peephole().keep();
- flag = init.flag();
- mem(flag==null ? bulk : mergeAlias(bulk,flag._alias,
- new ProjNode(init,2,memName(flag._alias)).peephole()));
- bulk.unkeep();
- init.unkeep();
- }
-
- // Field offset for a known type (but unknown offset)
- public static ConFldOffNode off( TypeStruct ts, String fname ) {
- return (ConFldOffNode)(new ConFldOffNode(ts,fname).peephole());
- }
- // Field offset for an unknown type
- public Node fldoff( Node n, String name ) {
- if( n._type instanceof TypeMemPtr ptr && ptr._obj._fref )
- _requiredTypes.add(ptr._obj._name);
- return new FldOffNode(n,name,loc()).peephole();
- }
- public Node peep( Node n ) {
- // Peephole, then improve with lexically scoped guards
- return _scope.upcastGuard(n.peephole());
- }
-
- // Parse a string or null
- private String parseString() {
- if( !peek('"') ) return null;
- _lexer.inc();
- StringBuilder sb = new StringBuilder();
- while( !_lexer.isEOF() ) {
- char c = _lexer.nextChar();
- if( c=='"' ) return sb.toString();
- if( c!='\\' ) {
- sb.append(c);
- continue;
- }
- if( _lexer.isEOF() )
- throw error("Unclosed string");
- sb.append(escapedChar("string"));
- }
- throw error("Unclosed string");
- }
-
- private char escapedChar(String kind) {
- char esc = _lexer.nextChar();
- return switch( esc ) {
- case '0' -> '\u0000';
- case 'n' -> '\n';
- case 't' -> '\t';
- case 'r' -> '\r';
- case '\\' -> '\\';
- case '"' -> '"';
- case '\'' -> '\'';
- default -> throw error("Unknown "+kind+" escape \\\\"+esc);
- };
- }
-
- // Already parsed "'"
- private Node parseChar() {
- if( _lexer.isEOF() ) throw error("Unclosed character");
- char c = _lexer.nextChar();
- if( c=='\\' ) {
- if( _lexer.isEOF() ) throw error("Unclosed character");
- c = escapedChar("character");
- }
- return require(con(TypeInteger.constant(c)),"'");
- }
-
- private boolean noLowerCase(String s) {
- for( int i=0; i<s.length(); i++ )
- if( Character.isLowerCase(s.charAt(i)) )
- return false;
- return true;
- }
-
-
- //////////////////////////////////
- // Utilities for lexical analysis
-
- // Return true and skip if "syntax" is next in the stream.
- private boolean match (String syntax) { return _lexer.match (syntax); }
- // Match must be "exact", not be followed by more id letters
- private boolean matchx(String syntax) { return _lexer.matchx(syntax); }
- private boolean matchOp(char c0 ) { return _lexer.matchOp(c0); }
- private boolean matchOpx(char c0, char c1) { return _lexer.matchOpx(c0,c1); }
- // Return true and do NOT skip if 'ch' is next
- private boolean peek(char ch) { return _lexer.peek(ch); }
- private boolean peekIsId() { return _lexer.peekIsId(); }
-
- // ch is +/- 1, means oper++ or oper-- means postfix
- private static boolean postfix(char ch) {
- return (byte)ch== 1 || (byte)ch== -1;
- }
-
- public int pos() { return _lexer._position; }
- private int pos(int pos) {
- int old = _lexer._position;
- _lexer._position = pos;
- return old;
- }
- private Type posT(int pos) { _lexer._position = pos; return null; }
- // Source code location
- Lexer loc() { return new Lexer(_lexer); }
-
-
- // Require and return an identifier
- private String requireId() {
- String id = _lexer.matchId();
- if (id != null && !KEYWORDS.contains(id) ) return id.intern();
- throw error("Expected an identifier, found '"+id+"'");
- }
-
- private String matchId() {
- int old = pos();
- String id = _lexer.matchId();
- if( id==null ) return null;
- if( !KEYWORDS.contains(id) ) return id;
- pos(old);
- return null;
- }
-
- // Require an exact match
- private Parser require(String syntax) { require(null, syntax); return this; }
- private <N> N require(N n, String syntax) {
- if (match(syntax)) return n;
- throw errorSyntax(syntax);
- }
-
- ////////////////////////////////////
- // Lexer components
-
- // Lexer provides low level access to the raw file bytes, peeks and matches
- // short strings, parses numbers, skips comments and whitespace, tracks
- // line numbers, allows the parse position to be saved and restored, and
- // serves as a location indicator for errors.
-
- public static class Lexer {
-
- // Input buffer; an array of text bytes read from a file or a string
- private final byte[] _input;
- public final CompUnit _cu; // Original source, retained across inlining.
- // Tracks current position in input buffer
- private int _position = 0;
-
- //
- private int _line_number = 1;
- // Start of current line
- private int _line_start = 0;
-
- /** True when this location precedes {@code that} in the same source. */
- public boolean before(Lexer that) {
- return that != null && (_cu!=null ? _cu==that._cu : _input==that._input) && _position < that._position;
- }
-
- public String position() { return _line_number+":"+(_position-_line_start+1); }
- public int pos() { return _position; }
-
- /**
- * Record the source text for lexing
- */
- public Lexer(String source) {
- this(source.getBytes());
- }
-
- /**
- * Direct from disk file source
- */
- public Lexer(byte[] buf) {
- _input = buf;
- _cu = null;
- }
-
- private Lexer(CompUnit cu) {
- _input = cu._src.getBytes();
- _cu = cu;
- }
-
- // Imported location: source text is only needed when reporting an error.
- private Lexer(CompUnit cu, int pos) {
- _input = null;
- _cu = cu;
- _position = pos;
- }
-
-
- public static void gather(Lexer loc, HashMap<String,Integer> strs) {
- if( loc==null || loc._cu==null ) return;
- Serialize.gather(strs,loc._cu._cname);
- String file=loc._cu.sourceFile();
- if( file!=null ) Serialize.gather(strs,file);
- }
-
- public static void packed(Lexer loc, BAOS out, HashMap<String,Integer> strs) {
- if( loc==null || loc._cu==null ) { out.packed4(0); return; }
- out.packed4(loc._position+1);
- out.packed4(strs.get(loc._cu._cname));
- String file=loc._cu.sourceFile();
- out.packed4(file==null ? 0 : strs.get(file));
- }
-
- public static Lexer unpack(BAOS in, String[] strs) {
- int pos=in.packed4();
- if( pos==0 ) return null;
- String owner=strs[in.packed4()];
- String file=strs[in.packed4()];
- CompUnit cu=CodeGen.CODE._compunits.get(owner.replace('.','/'));
- assert cu!=null : owner;
- if( !file.isEmpty() ) cu._sourceFile=file;
- return new Lexer(cu,pos-1);
- }
-
- /**
- * Copy a lexer from a lexer
- */
- private Lexer(Lexer l) {
- _input = l._input;
- _cu = l._cu;
- _position = l._position;
- _line_number = l._line_number;
- _line_start = l._line_start;
- }
-
- // Very handy in the debugger, shows the unparsed program
- @Override
- public String toString() {
- return _input==null ? _cu._cname+"@"+_position : new String(_input, _position, _input.length - _position);
- }
-
- // True if at EOF
- private boolean isEOF() {
- return _position >= _input.length;
- }
-
- // Peek next character, or report EOF
- private char peek() {
- return isEOF() ? Character.MAX_VALUE // Special value that causes parsing to terminate
- : (char) _input[_position];
- }
- // Just crash if misused
- public byte peek(int off) { return _input[_position+off]; }
-
- private void inc() {
- if( _position++ < _input.length && _input[_position-1] == '\n' ) {
- _line_number++;
- _line_start = _position;
- }
- }
- // Does not honor LF, so caller must roll back position on a LF
- private char nextChar() {
- char ch = peek();
- inc();
- return ch;
- }
-
- // True if a white space
- private boolean isWhiteSpace() {
- return peek() <= ' '; // Includes all the use space, tab, newline, CR
- }
-
- /**
- * Return the next non-white-space character
- */
- private void skipWhiteSpace() {
- while( true ) {
- if( isWhiteSpace() ) inc();
- // Skip // to end of line
- else if( _position+2 < _input.length &&
- _input[_position ] == '/' &&
- _input[_position+1] == '/') {
- inc(); inc();
- while( !isEOF() && _input[_position] != '\n' )
- inc();
- } else if( _position+2 < _input.length &&
- _input[_position ] == '/' &&
- _input[_position+1] == '*') {
- // Skip /*comment*/
- while( !isEOF() && !(_input[_position-1] == '*' && _input[_position] == '/'))
- inc();
- inc();
- } else break;
- }
- }
-
- // Next non-white-space character, or EOF
- public char nextXChar() { skipWhiteSpace(); return nextChar(); }
-
- // Return true, if we find "syntax" after skipping white space; also
- // then advance the cursor past syntax.
- // Return false otherwise, and do not advance the cursor.
- boolean match(String syntax) {
- assert syntax.indexOf('\n')==-1; // No newlines in match
- skipWhiteSpace();
- int len = syntax.length();
- if (_position + len > _input.length) return false;
- for (int i = 0; i < len; i++)
- if ((char) _input[_position + i] != syntax.charAt(i))
- return false;
- _position += len;
- return true;
- }
-
- // Match must be exact and not followed by more ID characters.
- // Prevents identifier "ifxy" from matching an "if" statement.
- boolean matchx(String syntax) {
- if( !match(syntax) ) return false;
- if( !isIdLetter(peek()) ) return true;
- _position -= syntax.length();
- return false;
- }
- // Match this char, and the next char must be different.
- // Handles '&&' vs '&'
- boolean matchOp( char c0 ) {
- skipWhiteSpace();
- if( _position+1 >= _input.length || _input[_position]!=c0 || _input[_position+1]==c0 )
- return false;
- inc();
- return true;
- }
- // Match these two characters in a row
- boolean matchOpx(char c0, char c1) {
- skipWhiteSpace();
- if( _position+1 >= _input.length || _input[_position]!=c0 || _input[_position+1]==c1 )
- return false;
- inc();
- return true;
- }
-
- private boolean peek(char ch) {
- skipWhiteSpace();
- return peek()==ch;
- }
-
- boolean peekIsId() {
- skipWhiteSpace();
- return isIdStart(peek());
- }
-
- // Return an identifier or null
- String matchId() {
- return peekIsId() ? parseId() : null;
- }
-
- // Used for errors
- String getAnyNextToken() {
- if (isEOF()) return "EOF";
- if (isIdStart(peek())) return parseId();
- if (isNumber(peek())) return parseNumberString();
- if (isPunctuation(peek())) return parsePunctuation();
- return String.valueOf(peek());
- }
-
-
- boolean isNumber(char ch) {return Character.isDigit(ch);}
-
- // Return a constant Type, either TypeInteger or TypeFloat
- private Type parseNumber() {
- int old = _position;
- int len = isLongOrDouble();
- boolean f32 = peek()=='f';
- if( f32 ) nextChar();
- int n = Math.abs(len);
- if( f32 )
- return TypeFloat.constant(Float.parseFloat(new String(_input,old,n)));
- if( len > 0 ) {
- if( len > 1 && _input[old]=='0' )
- throw error("Syntax error: integer values cannot start with '0'",this);
- long i = Long.parseLong(new String(_input,old,len));
- return TypeInteger.constant(i);
- }
- return TypeFloat.constant(Double.parseDouble(new String(_input,old,n)));
- }
- private String parseNumberString() {
- int old = _position;
- int len = Math.abs(isLongOrDouble());
- if( peek()=='f' ) {
- nextChar();
- len++;
- }
- return new String(_input,old,len);
- }
-
- // Return +len that ends a long
- // Return -len that ends a double
- private int isLongOrDouble() {
- int old = _position;
- char c;
- while( Character.isDigit(c=nextChar()) ) ;
- if( !(c=='e' || c=='.') )
- return --_position - old;
- if( peek() == '-' ) nextChar();
- while( Character.isDigit(c=nextChar()) || c=='e' || c=='.' ) ;
- return -(--_position - old);
- }
-
- // First letter of an identifier
- private boolean isIdStart(char ch) {
- return Character.isAlphabetic(ch) || ch == '_';
- }
-
- // All characters of an identifier, e.g. "_x123"
- private boolean isIdLetter(char ch) {
- return Character.isLetterOrDigit(ch) || ch == '_';
- }
-
- private String parseId() {
- int start = _position;
- while (isIdLetter(nextChar())) ;
- return new String(_input, start, --_position - start).intern();
- }
-
- //
- private boolean isPunctuation(char ch) {
- return "=;[]<>()+-/*&|^".indexOf(ch) != -1;
- }
-
- private String parsePunctuation() {
- int start = _position;
- return new String(_input, start, 1);
- }
-
- // Next oper= character, or 0.
- // As a convenience, mark "++" as a char 1 and "--" as char -1 (65535)
- // Distinct tags for the multi-character compound operators.
- static final char SHL = 2, SAR = 3, SHR = 4;
-
- public char matchOperAssign() {
- skipWhiteSpace();
- if( match("<<=" ) ) return SHL;
- if( match(">>>=") ) return SHR;
- if( match(">>=" ) ) return SAR;
- if( _position+2 >= _input.length ) return 0;
- char ch0 = (char)_input[_position];
- if( "+-/*&|^".indexOf(ch0) == -1 ) return 0;
- char ch1 = (char)_input[_position+1];
- if( ch1 == '=' ) { _position += 2; return ch0; }
- if( isIdLetter((char)_input[_position+2]) ) return 0;
- if( ch0 == '+' && ch1 == '+' ) { _position += 2; return (char) 1; }
- if( ch0 == '-' && ch1 == '-' ) { _position += 2; return (char)-1; }
- return 0;
- }
- }
-
- ParseException errorSyntax(String syntax) { return _errorSyntax("expected `"+syntax+"`"); }
- private ParseException _errorSyntax(String msg) {
- return error("Syntax error, "+msg+" but found `" + _lexer.getAnyNextToken()+"`");
- }
- ParseException error(String msg) { return error(msg,_lexer); }
- public static ParseException error(String msg, Lexer loc) { return new ParseException(msg,loc); }
-
- public static class ParseException extends RuntimeException {
- public final Lexer _loc;
- // file:line:charoff err
- //String msg = "src:"+_line_number+":"+(_position-_line_start)+" "+errorMessage;
- ParseException( String msg, Lexer loc ) { super(msg); _loc = loc; }
- }
-
-}
diff --git a/src/main/java/com/seaofnodes/simple/README.md b/src/main/java/com/seaofnodes/simple/README.md
new file mode 100644
index 00000000..613f66b4
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/README.md
@@ -0,0 +1,15 @@
+# Compiler source layout
+
+| Directory | Contents |
+| --- | --- |
+| `parser/` | Recursive-descent grammar routines, lexing, name resolution, and construction of the initial IR. `Var` describes a lexical binding. See the [grammar map](../../../../../../docs/chapter29/29-grammar.md). |
+| `node/` | Sea-of-Nodes IR nodes: graph edges, type computation, and local peephole rewrites. |
+| `node/cpus/` | Machine-specific nodes and instruction selection support for `arm`, `riscv`, and `x86_64_v2`. |
+| `codegen/` | Compilation driver and passes: module loading, optimization, scheduling, register allocation, serialization, and machine-code emission. `IterPeeps` runs the peephole worklist. |
+| `type/` | The type lattice, type interning, and representations of values, structs, functions, and memory. |
+| `print/` | IR/assembly printers and adapters for the shared graph viewer. |
+| `util/` | General-purpose collections, bit sets, byte buffers, and other small utilities. |
+
+The root package retains `sys.java`, which loads the default Simple namespace.
+Java tests, the command-line driver, and the IR evaluator live under
+`src/test/java`; the Simple standard library lives under `src/main/smp`.
diff --git a/src/main/java/com/seaofnodes/simple/codegen/ClassInitDependencies.java b/src/main/java/com/seaofnodes/simple/codegen/ClassInitDependencies.java
index 9a544685..9c954197 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/ClassInitDependencies.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/ClassInitDependencies.java
@@ -1,6 +1,7 @@
package com.seaofnodes.simple.codegen;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Lexer;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.*;
@@ -76,7 +77,7 @@ public class ClassInitDependencies {
// Return the outgoing init edge set for this function
private static int[] walk( CodeGen code, FunNode fun, Ary<CFGNode> work, IntHashMap iset, BitSet visit,
- Ary<Parser.Lexer> sites) {
+ Ary<Lexer> sites) {
if( fun.summary()!=null ) return fun.summary()._classDependencyFIDXs;
// Walk function body, looking for outgoing init edges
assert iset .isEmpty(); // int hash set for de-dup
@@ -99,7 +100,9 @@ public class ClassInitDependencies {
// CLInit adds a class init requirement to this function
// (plus all parent classes)
- if( cfg instanceof CLInitNode init ) {
+ // An invalid receiver can still be Scalar.BOT here. Its Load
+ // reports the error at TypeCheck; it does not name a class to init.
+ if( cfg instanceof CLInitNode init && init.cu()!=null ) {
FunNode clz = code.link(init.cu()); // <clinit> referred to
iset.put(clz.sig().fidx(),1); // Add to edge set
if( sites!=null && sites.atX(clz.sig().fidx())==null ) sites.setX(clz.sig().fidx(),init._loc);
@@ -164,7 +167,7 @@ public class ClassInitDependencies {
Ary<CFGNode> work = new Ary<>(CFGNode.class);
IntHashMap iset = new IntHashMap(0);
BitSet visit = new BitSet();
- Ary<Parser.Lexer> sites = new Ary<>(Parser.Lexer.class);
+ Ary<Lexer> sites = new Ary<>(Lexer.class);
for( int i=idx; i<_sp; i++ ) {
int src = _stk[i];
FunNode fun = _code.link(src);
@@ -175,7 +178,7 @@ public class ClassInitDependencies {
for( int dst : _edgess[src] )
if( members.get(dst) ) {
sb.p("\n ").p(name(src)).p(" -> ").p(name(dst));
- Parser.Lexer loc = sites.atX(dst);
+ Lexer loc = sites.atX(dst);
if( loc!=null && loc._cu!=null )
sb.p('\n').p(SourceError.format(loc._cu,loc.pos(),"cyclic class initialization"));
else if( fun!=null && fun.isClz() && dst!=0 && fun._compunit._par==_code.link(dst)._compunit )
diff --git a/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java b/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java
index 998989b1..b4c93e1e 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/CodeGen.java
@@ -1,8 +1,7 @@
package com.seaofnodes.simple.codegen;
import com.seaofnodes.graph.GraphObserver;
-import com.seaofnodes.simple.IterPeeps;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.print.*;
import com.seaofnodes.simple.type.*;
@@ -98,7 +97,7 @@ public class CodeGen {
}
public static boolean hasUserConstructor(TypeStruct self) {
- Type t = Parser.TYPES.get(Parser.addClzPrefix(self._name));
+ Type t = Parser.TYPES.get(TypeStruct.addClzPrefix(self._name));
return t instanceof TypeStruct clz && clz.field("<ctor>") != null;
}
@@ -248,7 +247,7 @@ public class CodeGen {
}
public String entryClinitName() {
- return Parser.addClzPrefix(_srcName==null ? "Test" : _srcName.replace('/','.'))+".<clinit>";
+ return TypeStruct.addClzPrefix(_srcName==null ? "Test" : _srcName.replace('/','.'))+".<clinit>";
}
// Verbose printing during compilation
@@ -367,7 +366,12 @@ public class CodeGen {
// being called.
public void link(FunNode fun) {
int fidx = fun.sig().fidx();
+ FunNode old = _linker.atX(fidx);
+ if( old!=null && old!=fun ) old.moveDepsToWorklist();
_linker.setX(fidx,fun);
+ // Calls waiting for an unloaded target depend on Start until there
+ // is a FunNode to depend on. Installing a body lets them retry.
+ if( _start!=null ) _start.moveDepsToWorklist();
add(_globalEscapes); // A new or replacement summary may expose escapes.
}
@@ -729,7 +733,7 @@ public class CodeGen {
// Publish definitions only; referenced classes belong to other objects.
for( String fname : elf._units )
- _externSymbols.putIfAbsent(Parser.addClzPrefix(fname.replace('/','.')),elf);
+ _externSymbols.putIfAbsent(TypeStruct.addClzPrefix(fname.replace('/','.')),elf);
// A -L object can name dependencies alongside itself. They
// need metadata discovery even when no directory was supplied.
for( String dep : elf._deps ) {
diff --git a/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java b/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java
index 8b303008..995301c3 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/CompUnit.java
@@ -1,6 +1,5 @@
package com.seaofnodes.simple.codegen;
-import com.seaofnodes.simple.Parser;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.Type;
import com.seaofnodes.simple.type.TypeStruct;
@@ -135,7 +134,7 @@ public class CompUnit {
static File dependencyObj(String fname, boolean local) {
File obj = new File(CODE._buildDir+"/"+fname+".o");
if( local && (obj.isFile() || new File(CODE._modDir+"/"+fname+".smp").isFile()) ) return obj;
- ElfReader elf = CODE.findExternalSimple(Parser.addClzPrefix(fname.replace('/','.')));
+ ElfReader elf = CODE.findExternalSimple(TypeStruct.addClzPrefix(fname.replace('/','.')));
return elf==null ? null : elf._file;
}
diff --git a/src/main/java/com/seaofnodes/simple/codegen/Encoding.java b/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
index 067f431a..b07a8fec 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/Encoding.java
@@ -1,6 +1,5 @@
package com.seaofnodes.simple.codegen;
-import com.seaofnodes.simple.Parser;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.print.IRPrinter;
import com.seaofnodes.simple.type.*;
@@ -66,7 +65,7 @@ public class Encoding implements com.seaofnodes.isa.CodeSink {
// Class initializers still write their fields, even when every
// final value is constant. Their storage must remain writable.
return !(t instanceof TypeStruct ts) ||
- (!Parser.startsClzPrefix(ts._name) && ts.isConstant());
+ (!TypeStruct.startsClzPrefix(ts._name) && ts.isConstant());
}
}
@@ -201,7 +200,7 @@ public class Encoding implements com.seaofnodes.isa.CodeSink {
// addressing to load it. Type is stored in either the .rodata or .data.
public void largeConstant( Node relo, Type t, int off, int elf ) {
// TODO: Any-old struct in the cpool
- assert t.isConstant() || (t instanceof TypeStruct ts && Parser.startsClzPrefix(ts._name));
+ assert t.isConstant() || (t instanceof TypeStruct ts && TypeStruct.startsClzPrefix(ts._name));
assert (byte)off == off;
assert (byte)elf == elf;
_bigCons.put(relo,new Relo(relo,t,(byte)off,(byte)elf));
diff --git a/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java b/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java
index a1ac4e67..86cb2e98 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/GlobalCodeMotion.java
@@ -1,6 +1,6 @@
package com.seaofnodes.simple.codegen;
-import com.seaofnodes.simple.IterPeeps.WorkList;
+import com.seaofnodes.simple.codegen.IterPeeps.WorkList;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.Ary;
diff --git a/src/main/java/com/seaofnodes/simple/codegen/Iter.java b/src/main/java/com/seaofnodes/simple/codegen/Iter.java
index 5b4fb983..35bc62ac 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/Iter.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/Iter.java
@@ -11,38 +11,14 @@ public abstract class Iter {
assert code._globalEscapes==null;
code._globalEscapes = new GlobalEscapeNode();
code._start.setDef(2,code._globalEscapes);
- // The phase shift closes all parser-in-progress states and freezes
- // unknown external inputs. Imported nodes carry post-Opto escape
- // precision, but Iter is a fresh pessimistic solve: memory escape
- // summaries must restart at FULL so newly discovered private escapes
- // can only be removed, never added. Seed the complete graph once.
- boolean hasImports = code._externPaths != null;
- code._start.walk(n -> {
- // Type import can reconcile a constant's declaration.
- n._type = n instanceof ConstantNode ? n.compute()
- : hasImports ? iterImportReset(n._type) : iterEscapeReset(n._type);
- code.add(n);
- return null;
- });
+ // Parsing queues initialized nodes and phase-sensitive peepholes.
+ // Only the new escape summary needs seeding here.
+ code.add(code._globalEscapes);
code._iter.iterate(code);
// Expensive assert.
assert !CodeGen.expensiveAssert() || Opto.fixedPointCheck(code);
}
- private static Type iterEscapeReset(Type t) {
- if( t instanceof TypeMem mem )
- return mem._one ? mem : mem.makeFrom(XInt.FULL,XInt.FULL);
- if( t instanceof TypeTuple tt ) {
- TypeTuple rez = tt;
- for( int i=0; i<tt._types.length; i++ ) {
- Type tx = iterEscapeReset(tt._types[i]);
- if( tx != tt._types[i] ) rez = rez.makeFrom(i,tx);
- }
- return rez;
- }
- return t;
- }
-
private static Type iterImportReset(Type t) {
if( t.isHigh() )
return t.dual();
@@ -69,17 +45,13 @@ public abstract class Iter {
for( Node n : nodes )
if( !n.isDead() )
n._type = n instanceof ConstantNode ? n.compute() : iterImportReset(n._type);
- // Recompute each imported unit's contribution from its reset inputs.
- for( Node n : nodes )
- if( n instanceof StopCUNode && !n.isDead() ) n._type = n.compute();
code.addAll(nodes);
repair(code);
}
- // Repair the monotonicity assert after loading new nodes (from a file via
- // Serialize). Only widening types here makes it safe to resume the usual
- // lifting peeps. The worklist already contains changed edges and their
- // neighbors.
+ // Loading a body or linking a call changes the graph and can widen
+ // existing bounds. Repair only queued nodes and affected neighbors before
+ // resuming the usual lifting peeps; this does not walk the graph.
public static void repair(CodeGen code) {
// Gather changes for a normal IterPeeps once the graph has its
// monotonicity property back.
diff --git a/src/main/java/com/seaofnodes/simple/IterPeeps.java b/src/main/java/com/seaofnodes/simple/codegen/IterPeeps.java
index a018c3ee..985cd4d2 100644
--- a/src/main/java/com/seaofnodes/simple/IterPeeps.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/IterPeeps.java
@@ -1,7 +1,5 @@
-package com.seaofnodes.simple;
+package com.seaofnodes.simple.codegen;
-import com.seaofnodes.simple.codegen.CodeGen;
-import com.seaofnodes.simple.codegen.Opto;
import com.seaofnodes.simple.type.Type;
import com.seaofnodes.simple.type.TypeInteger;
import com.seaofnodes.simple.util.Ary;
@@ -76,9 +74,9 @@ public class IterPeeps {
// Pick an inline candidate, no real heuristic, first come, first served
boolean inlined = false;
CallEndNode cend;
- while( (cend=_workInline.pop()) != null) {
+ while( (cend = _workInline.pop()) != null) {
if( cend.isDead() ) continue;
- if( cend.loadForInline() ) { inlined=true; break; }
+ if( cend.loadForInline() ) { inlined = true; break; }
byte inline = cend.maybeInline();
if( inline == -1 ) defer.add(cend);
if( inline > 0 ) {
@@ -102,7 +100,7 @@ public class IterPeeps {
assert !CodeGen.expensiveAssert(0) || (progressOnList(code, _work) && schedulableUses(code));
Node n;
- while( (n=_work.pop()) != null ) {
+ while( (n = _work.pop()) != null ) {
if( n.isDead() ) continue;
_cnt++; // Useful for debugging, searching which peephole broke things
var obs = code._obs; // Collect peephole neighbors for graph display
@@ -167,7 +165,7 @@ public class IterPeeps {
Node changed = code._stop.walk( n -> {
// Ignore most in-progress things
if( n.iskeep() ) return null;
- // Types must be forwards, even if on the worklist.
+ // Types must lift, even if on the worklist.
Type nval = n.compute();
assert nval.isa(n._type) : "Non-monotonic peep: "+n+"#"+n._nid+" old="+n._type+" new="+nval+" inputs="+inputTypes(n)+" peep="+n;
if( list.on(n) )
@@ -230,7 +228,7 @@ public class IterPeeps {
return null;
if( use instanceof PhiNode phi ) {
CFGNode found = null;
- for( int i=1; i<phi.nIns(); i++ )
+ for( int i = 1; i<phi.nIns(); i++ )
if( phi.in(i)==n ) {
if( i >= phi.region().nIns() )
return null;
@@ -247,7 +245,7 @@ public class IterPeeps {
.append(" early=").append(cfg(early))
.append(" use-lca=").append(cfg(lca))
.append(" node=").append(n).append("\ninputs:");
- for( int i=0; i<n.nIns(); i++ ) {
+ for( int i = 0; i<n.nIns(); i++ ) {
Node in = n.in(i);
sb.append("\n in").append(i).append(": ").append(node(in))
.append(" cfg=").append(cfg(CFGNode.safeCFG(in)));
diff --git a/src/main/java/com/seaofnodes/simple/codegen/ListScheduler.java b/src/main/java/com/seaofnodes/simple/codegen/ListScheduler.java
index 3b1d3182..c3b603e5 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/ListScheduler.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/ListScheduler.java
@@ -1,6 +1,6 @@
package com.seaofnodes.simple.codegen;
-import com.seaofnodes.simple.IterPeeps.WorkList;
+import com.seaofnodes.simple.codegen.IterPeeps.WorkList;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.Ary;
diff --git a/src/main/java/com/seaofnodes/simple/codegen/Opto.java b/src/main/java/com/seaofnodes/simple/codegen/Opto.java
index 8c8827c6..6d653835 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/Opto.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/Opto.java
@@ -1,7 +1,8 @@
package com.seaofnodes.simple.codegen;
import com.seaofnodes.simple.node.*;
-import com.seaofnodes.simple.IterPeeps.WorkList;
+import com.seaofnodes.simple.parser.Parser;
+import com.seaofnodes.simple.codegen.IterPeeps.WorkList;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.Ary;
@@ -54,28 +55,73 @@ abstract public class Opto {
// After SCCP add everything changed to the worklist and see if
// peepholes can make more progress.
moveChangesToWorklist(code, oldTypes, changes);
+ // Unknown callers and concrete call edges have now been rediscovered.
for( ReturnNode ret : returns ) code.add(ret.unkeep());
}
- // Reset Types before running the worklist algorithm
+ // Transition from Iter's pessimistic graph to SCCP's optimistic solve in
+ // one walk. Parsing has closed the named types; install them in the IR
+ // and resolve forward references before resetting each value to TOP.
+ // Do not run peepholes here: neighboring nodes may not yet be upgraded or
+ // reset. SCCP sees the completed setup, and IterPostOpto handles rewrites.
+ //
+ // The same walk preserves Returns while call edges are rebuilt, remembers
+ // old types for incremental cleanup, and gathers numeric modes and phase
+ // dependencies. These are setup duties, not separate graph passes.
private static void resetToTop(CodeGen code, Ary<Type> oldTypes, Ary<Node> modes, WorkList<Node> deps, Ary<ReturnNode> returns ) {
code._start.walk( x -> {
- // Keep bodies alive while SCCP rebuilds their callers.
+ // A snapshot for change detection, not an upper bound: closing
+ // recursive declarations can change types in either direction.
+ oldTypes.setX(x._nid,x._type);
+ if( x.upgradeType(Parser.TYPES) ) deps.push(x);
+ if( x instanceof FRefNode fref ) resolveForward(code,fref);
+ // Rebuilding call edges must not DCE a temporarily uncalled body.
+ // Type-level function references can rediscover it later in SCCP.
if( x instanceof ReturnNode ret ) returns.add(ret.keep());
// Opto ends Parm.inProgress(), enabling constant folding even
// when a parameter's type was already constant before SCCP.
if( x instanceof ParmNode && x._type.isHighOrConst() ) deps.push(x);
+ // Construction identities may fold after the optimistic solve.
+ // Adds may need sorting after imported node IDs were assigned.
+ if( x instanceof ConstructNode || x instanceof InitViewNode || x instanceof AddNode ) deps.push(x);
// SCCP adds call edges and default arguments, not numeric
// operations. Gather unresolved modes on this required walk.
if( x instanceof ModeNode mode && mode.mode()==0 ) modes.push(x);
- // Change snapshot, not a bound: Opto adds foreign identities.
- oldTypes.setX(x._nid,x._type);
x._type = Type.TOP; // Reset all Nodes to TOP
code._iter.add(x); // Visit everybody at least once; most Nodes produce better than TOP
return null;
} );
}
+ private static void resolveForward(CodeGen code, FRefNode fref) {
+ if( fref.nIns()!=1 ) return;
+ boolean alloc = fref._name.endsWith(".<new>");
+ String clzName = alloc
+ ? fref._name.substring(0,fref._name.length()-".<new>".length()).intern()
+ : fref._name;
+ TypeStruct clz = (TypeStruct)Parser.TYPES.get(clzName);
+ if( clz==null ) return;
+ Node con;
+ if( alloc ) {
+ Field ctor = clz.field("<new>");
+ if( ctor==null || !(ctor._t instanceof TypeFunPtr tfp) ) {
+ String name = clzName.startsWith(TypeStruct.CLZ)
+ ? clzName.substring(TypeStruct.CLZ.length()) : clzName;
+ int dot = name.lastIndexOf('.');
+ throw Parser.error("Unknown struct type '"+(dot==-1 ? name : name.substring(dot+1))+"'",fref._loc);
+ }
+ FunNode fun = code._link(tfp);
+ if( fun==null ) return;
+ con = new FunPtrNode(tfp,code._start,fun.ret());
+ con._type = Type.BOTTOM; // No Iter bound; the Return may already be reset.
+ } else {
+ con = new ConstantNode(TypeMemPtr.make((byte)2,clz,true));
+ }
+ // The new input is visited by the reset walk too. Let SCCP propagate
+ // its value; replacing the FRef is a later peephole.
+ fref.addDef(con);
+ }
+
// Only entry initializers start with unknown callers. Discover every
// other function through calls or escaping values during SCCP.
@@ -98,6 +144,8 @@ abstract public class Opto {
// Unhook from Stop without treating the ReturnNode as
// DEAD. It stays in limbo until Opto links when a caller
// is discovered (or not, and the function is really dead).
+ // The reset walk keeps Returns alive. Use normal edge
+ // editing so cleanup also revisits an emptied StopCU.
stop.delDef(i--);
}
// Unlink the existing conservative call linkages.
@@ -113,8 +161,9 @@ abstract public class Opto {
private static void linkCG(CodeGen code, TypeFunPtr tfp, CallNode call) {
if( tfp.nargs() != call.nargs() ) return; // Error calls hit this
int[] fidxs = tfp.fidxs();
- // Infinite targets use the outside continuation. Escaped local
- // callbacks are reached through Start, and their effects feed Stop.
+ // An infinite target set uses the outside continuation. Escaped local
+ // callbacks are called through Start; their effects feed that same
+ // outside state. Do not attach unrelated same-arity function bodies.
if( XInt.isHigh(fidxs) ) return;
for( int fidx = XInt.next(fidxs,0); fidx >=0; fidx = XInt.next(fidxs,fidx) ) {
// unlinkStart deliberately leaves uncalled functions in a
@@ -137,7 +186,8 @@ abstract public class Opto {
TypeMem tmem = (TypeMem)tt._types[1];
int[] fidxs = tmem._escFs;
if( XInt.isHigh(fidxs) ) {
- // Cofinite sets include foreign identities but can exclude locals.
+ // A cofinite set includes foreign identities but can exclude local
+ // functions. Honor those exclusions just as in the finite case.
for( FunNode fun : code._linker )
if( fun != null && !fun.isDead() && XInt.bit(fidxs,fun.sig().fidx()) )
linkStart(code,fun);
@@ -151,7 +201,8 @@ abstract public class Opto {
}
// Function is now reachable by unknown callers through Start. The
- // memory and callback inputs read Start; hidden self uses its owning class.
+ // Memory and callback defaults read Start's changing outside state.
+ // Scalar defaults remain constants; hidden self uses the owning class.
static void linkStart( CodeGen code, FunNode fun ) {
assert !fun.isDead();
StartCUNode start = fun._compunit._start;
diff --git a/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java b/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java
index a6b996f7..5f8a0ec0 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/ParseAll.java
@@ -1,7 +1,6 @@
package com.seaofnodes.simple.codegen;
-import com.seaofnodes.simple.Parser;
-import com.seaofnodes.simple.IterPeeps;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.print.*;
import com.seaofnodes.simple.type.*;
@@ -118,50 +117,7 @@ public abstract class ParseAll {
Parser.TYPES.put(ts._name,ts);
for( CompUnit cu : code._compunits.values() )
if( cu._ext==null )
- cu._clz = (TypeStruct)Parser.TYPES.get(Parser.addClzPrefix(cu._cname));
-
- // Walk over all Nodes, and upgrade the internal constants to the
- // closed-over types.
- code._stop.walk( (Node n) -> {
- n.upgradeType(Parser.TYPES);
- if( n instanceof FRefNode fref ) {
- boolean alloc = fref._name.endsWith(".<new>");
- String clzName = alloc
- ? fref._name.substring(0,fref._name.length()-".<new>".length()).intern()
- : fref._name;
- TypeStruct clz = (TypeStruct)Parser.TYPES.get(clzName);
- if( clz == null )
- return null;
- Node con;
- if( alloc ) {
- Field ctor = clz.field("<new>");
- if( ctor == null || !(ctor._t instanceof TypeFunPtr tfp) ) {
- String name = clzName.startsWith(Parser.CLZ)
- ? clzName.substring(Parser.CLZ.length()) : clzName;
- int dot = name.lastIndexOf('.');
- throw Parser.error("Unknown struct type '"+(dot==-1 ? name : name.substring(dot+1))+"'",fref._loc);
- }
- FunNode fun = code.link(tfp);
- if( fun == null ) return null;
- con = new FunPtrNode(tfp,code._start,fun.ret()).peephole();
- } else {
- // Class pointer constant:
- TypeMemPtr clzptr = TypeMemPtr.make((byte)2,clz,true);
- con = code.con(clzptr);
- }
- // Replace and optimize
- fref.addDef(con);
- code.add(fref);
- }
- // Adds sort by NIDs, which just got shuffled
- if( n instanceof AddNode add )
- code.add(add);
- // All imported bodies are loaded; link them before Iter checks types.
- if( n instanceof CallNode call && !NEEDS_LOAD.isEmpty() )
- code.add(call).idealize();
- return null;
- } );
-
+ cu._clz = (TypeStruct)Parser.TYPES.get(TypeStruct.addClzPrefix(cu._cname));
}
// Parse one Simple source code file. Add all the FRefs produced to the worklist.
@@ -181,7 +137,7 @@ public abstract class ParseAll {
xcunit = findCUnitExternalSimple(code,fref._name);
if( xcunit != null ) {
// Replace the FRef with the discovered class name.
- fref._name = Parser.addClzPrefix(xcunit._cname);
+ fref._name = TypeStruct.addClzPrefix(xcunit._cname);
cunit.addDep(xcunit);
} else {
throw new RuntimeException("Undefined name '" + fref._name +"'");
@@ -258,7 +214,7 @@ public abstract class ParseAll {
private static File externalObj(CodeGen code, CompUnit par, String name) {
String cname = par==null ? name : par._cname + "." + name;
- ElfReader elf = code.findExternalSimple(Parser.addClzPrefix(cname));
+ ElfReader elf = code.findExternalSimple(TypeStruct.addClzPrefix(cname));
return elf == null ? null : elf._file;
}
@@ -283,7 +239,7 @@ public abstract class ParseAll {
ProjNode oldMem = oldStart==null ? null : oldStart.proj(1);
if( oldMem!=null ) code.releaseReplacedNode(oldMem);
elf.loadSimple(code);
- cunit._clz = (TypeStruct)Parser.TYPES.get(Parser.addClzPrefix(cunit._cname));
+ cunit._clz = (TypeStruct)Parser.TYPES.get(TypeStruct.addClzPrefix(cunit._cname));
StartNode lstart = (StartNode)elf._nodes.at(0);
StopNode lstop = ( StopNode)elf._nodes.last();
@@ -334,9 +290,11 @@ public abstract class ParseAll {
// Add the new top-level loaded class
Parser.TYPES.put(cunit._clz._name,cunit._clz);
- Field inst = cunit._clz.field(cunit._name);
- if( inst != null && inst._t instanceof TypeFunPtr tfp && tfp._ret instanceof TypeMem mem )
- Parser.TYPES.put(cunit._cname,mem._t);
+ // Layout belongs to the allocator's object result. An initializer's
+ // private-memory return no longer promises initialized field values.
+ Field inst = cunit._clz.field("<new>");
+ if( inst != null && inst._t instanceof TypeFunPtr tfp && tfp._ret instanceof TypeMemPtr ptr )
+ Parser.TYPES.put(cunit._cname,ptr._obj);
}
// Symbol not found in source code (or we would not be here).
@@ -416,8 +374,8 @@ public abstract class ParseAll {
// Search external Simple objects only. C linkage uses explicit "C"
// declarations, which produce ExternNodes and are resolved by the linker.
public static CompUnit findCUnitExternalSimple( CodeGen code, String symbol ) {
- String clzName = symbol.startsWith(Parser.CLZ) ? symbol : Parser.addClzPrefix(symbol);
- String cname = clzName.substring(Parser.CLZ.length());
+ String clzName = symbol.startsWith(TypeStruct.CLZ) ? symbol : TypeStruct.addClzPrefix(symbol);
+ String cname = clzName.substring(TypeStruct.CLZ.length());
String fname = cname.replace('.','/');
CompUnit cunit = code._compunits.get(fname);
diff --git a/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java b/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java
index ac792988..f1aa18ef 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/RegAlloc.java
@@ -425,6 +425,21 @@ public class RegAlloc {
Node[] conflicts = lrg._selfConflicts.keySet().toArray(new Node[0]);
Arrays.sort(conflicts, (x,y) -> x._nid - y._nid );
+ // A commutative two-address tie may join a whole chain to a Phi even
+ // when only the Phi and an earlier def actually overlap. Reuse the
+ // failed-range walk to find those ties before inserting copies.
+ // A Split's single use is artificial: leave earlier splitting intact.
+ findAllLRG(lrg);
+ for( int i = 0; i<_ns._len; i++ ) {
+ Node n = _ns.at(i);
+ if( lrg(n)==lrg && n instanceof MachNode mach && mach.twoAddress()==1 &&
+ mach.commutes() && !(n.in(2) instanceof SplitNode) &&
+ n.in(2).nOuts()==1 && lrg(n.in(2))!=lrg )
+ n.swap12();
+ else _ns.remove(i--);
+ }
+ // Reuse the scratch list to remember just the ties we broke.
+
// First encounter with a loop Phi: try only cold splits this round.
// Remember the Phi across rounds so a persistent self-conflict forces
// the hot def/backedge splits on the next encounter.
@@ -466,7 +481,9 @@ public class RegAlloc {
insertBefore(phi,i,"use/self/phi",round,lrg);
}
// Split before two-address ops which extend the live range
- if( def instanceof MachNode mach && mach.twoAddress()!= 0 )
+ // A commuted operand already broke this tie. Rebuild it in the
+ // next normal coloring round instead of copying the old range.
+ if( def instanceof MachNode mach && mach.twoAddress()!=0 && _ns.find(def)==-1 )
insertBefore(def,mach.twoAddress(),"use/self/two",round,lrg);
}
return true;
diff --git a/src/main/java/com/seaofnodes/simple/codegen/Serialize.java b/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
index 98350a95..ae4ecb74 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
@@ -1,6 +1,6 @@
package com.seaofnodes.simple.codegen;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.*;
@@ -38,6 +38,7 @@ public class Serialize {
}
static void serialize( CodeGen code ) {
+ code.P.checkConstructionSerializable();
// Anonymous functions also need names when another object refers to them.
for( FunNode fun : code._linker )
if( fun!=null && !fun.isDead() && fun._name==null )
@@ -313,8 +314,8 @@ public class Serialize {
// A nested declaration belongs to its nearest enclosing file.
private static CompUnit classOwner(CodeGen code, TypeStruct ts) {
- if( !Parser.startsClzPrefix(ts._name) ) return null;
- String name = ts._name.substring(Parser.CLZ.length()).replace('.','/');
+ if( !TypeStruct.startsClzPrefix(ts._name) ) return null;
+ String name = ts._name.substring(TypeStruct.CLZ.length()).replace('.','/');
while( true ) {
CompUnit cu = code._compunits.get(name);
if( cu!=null ) return cu;
@@ -418,14 +419,12 @@ public class Serialize {
if( type instanceof TypeStruct ts )
Parser.TYPES.put(ts._name,ts);
for( Type type : types ) {
- if( !(type instanceof TypeStruct ts) || !Parser.startsClzPrefix(ts._name) )
+ if( !(type instanceof TypeStruct ts) || !TypeStruct.startsClzPrefix(ts._name) )
continue;
- String cname = ts._name.substring(Parser.CLZ.length());
- int dot = cname.lastIndexOf('.');
- String name = dot == -1 ? cname : cname.substring(dot+1);
- Field inst = ts.field(name.intern());
- if( inst != null && inst._t instanceof TypeFunPtr tfp && tfp._ret instanceof TypeMem mem && mem._t instanceof TypeStruct its )
- Parser.TYPES.put(cname,its);
+ String cname = ts._name.substring(TypeStruct.CLZ.length());
+ Field inst = ts.field("<new>");
+ if( inst != null && inst._t instanceof TypeFunPtr tfp && tfp._ret instanceof TypeMemPtr ptr )
+ Parser.TYPES.put(cname,ptr._obj);
}
elf._pubTypes = new TypeStruct[npubTypes];
for( int i=0; i<npubTypes; i++ ) {
diff --git a/src/main/java/com/seaofnodes/simple/codegen/StaticData.java b/src/main/java/com/seaofnodes/simple/codegen/StaticData.java
index a4147576..5d1a519b 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/StaticData.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/StaticData.java
@@ -1,6 +1,6 @@
package com.seaofnodes.simple.codegen;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.BAOS;
@@ -41,11 +41,11 @@ public final class StaticData {
}
static boolean isClass(Type t) {
- return t instanceof TypeStruct ts && Parser.startsClzPrefix(ts._name);
+ return t instanceof TypeStruct ts && TypeStruct.startsClzPrefix(ts._name);
}
static CompUnit owner(CodeGen code,TypeStruct ts) {
- return owner(code,ts._name.substring(Parser.CLZ.length()));
+ return owner(code,ts._name.substring(TypeStruct.CLZ.length()));
}
static CompUnit owner(CodeGen code,String name) {
if(name==null) return null;
@@ -65,7 +65,7 @@ public final class StaticData {
if( clz && Parser.TYPES.get(((TypeStruct)t)._name) instanceof TypeStruct canonical ) t=canonical;
CompUnit owner=clz ? owner(enc._code,(TypeStruct)t)
: t instanceof TypeFunPtr fp ? owner(enc._code,enc._code._fidxs.owner(fp.fidx())) : null;
- String symbol=clz ? ((TypeStruct)t)._name.substring(Parser.CLZ.length())+".$class"
+ String symbol=clz ? ((TypeStruct)t)._name.substring(TypeStruct.CLZ.length())+".$class"
: t instanceof TypeFunPtr fp ? functionSymbol(fp) : prefix+objects.size();
ObjectData obj=new ObjectData(t,symbol,!clz && (!(t instanceof TypeStruct ts) || ts.isConstant()),
owner!=null && !enc._code.emits(owner));
diff --git a/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java b/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java
index a5eaa5ca..a86cd18a 100644
--- a/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java
+++ b/src/main/java/com/seaofnodes/simple/codegen/TypeCheck.java
@@ -1,8 +1,9 @@
package com.seaofnodes.simple.codegen;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.Type;
+import com.seaofnodes.simple.type.TypeScalar;
import com.seaofnodes.simple.util.Ary;
// Reject invalid optimized programs before lowering erases their source-level
@@ -18,10 +19,14 @@ abstract class TypeCheck {
code._classInitDependencies = deps; // Serialize reuses this checked graph.
Ary<Node> errs = new Ary<>(Node.class);
+ Ary<ConstructNode> constructs = new Ary<>(ConstructNode.class);
+ Ary<InitViewNode> views = new Ary<>(InitViewNode.class);
code._stop.walk( n -> {
if( n.err() != null )
errs.add(n);
if( n instanceof CLInitNode ) code.add(n);
+ if( n instanceof ConstructNode con ) constructs.add(con);
+ if( n instanceof InitViewNode view ) views.add(view);
return null;
});
@@ -29,30 +34,42 @@ abstract class TypeCheck {
// Prefer an originating error over a secondary error caused by a
// BOTTOM input. If all errors are downstream-poisoned, retain
// the old first-error behavior.
- Node min = null;
- for( Node n : errs )
- if( noBottomInputs(n) )
- { min = n; break; }
- if( min==null )
- min = errs.at(0);
+ Node min = errs.at(0);
for( Node n : errs )
if( n!=min &&
- noBottomInputs(n) &&
+ (errorRank(n)<errorRank(min) || (errorRank(n)==errorRank(min) &&
((n instanceof MemOpNode nmop &&
min instanceof MemOpNode mmop &&
nmop._loc.before(mmop._loc)) ||
+ (n instanceof ConstructNode nc && min instanceof ConstructNode mc &&
+ nc._loc!=null && nc._loc.before(mc._loc)) ||
(n instanceof CFGNode ncfg &&
min instanceof CFGNode mincfg &&
- ncfg.idepth() < mincfg.idepth())) )
+ ncfg.idepth() < mincfg.idepth())))) )
min = n;
if( code._obs != null ) code._obs.error(min, min.err().getMessage());
throw min.err();
}
+ // Checked construction boundaries and views have no runtime behavior. Gathered
+ // above during the existing error walk; no separate graph pass/solve.
+
+ for( ConstructNode con : constructs )
+ if( !con.isDead() ) con.subsume(con.in(1));
+ for( InitViewNode view : views )
+ if( !view.isDead() ) view.subsume(view.in(1));
+ }
+
+ private static int errorRank(Node n) {
+ // Bad memory access originates the error. Missing construction fields
+ // in turn explain generic-scalar arithmetic further downstream.
+ if( n instanceof MemOpNode && noBottomInputs(n) ) return 0;
+ if( n instanceof ConstructNode ) return 1;
+ return noBottomInputs(n) ? 2 : 3;
}
private static boolean noBottomInputs(Node n) {
for( Node def : n._inputs )
- if( def!=null && !(def instanceof FldOffNode) && def._type==Type.BOTTOM )
+ if( def!=null && !(def instanceof FldOffNode) && (def._type==Type.BOTTOM || def._type==TypeScalar.BOT) )
return false;
return true;
}
diff --git a/src/main/java/com/seaofnodes/simple/node/AndNode.java b/src/main/java/com/seaofnodes/simple/node/AndNode.java
index f02007bc..a1c6bba1 100644
--- a/src/main/java/com/seaofnodes/simple/node/AndNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/AndNode.java
@@ -1,11 +1,12 @@
package com.seaofnodes.simple.node;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Lexer;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.type.Type;
import com.seaofnodes.simple.type.TypeInteger;
public class AndNode extends ArithNode {
- public AndNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); }
+ public AndNode(Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); }
@Override public Tag serialTag() { return Tag.And; }
@Override public String op() { return "&"; }
diff --git a/src/main/java/com/seaofnodes/simple/node/ArithNode.java b/src/main/java/com/seaofnodes/simple/node/ArithNode.java
index e2a88944..2c6610e1 100644
--- a/src/main/java/com/seaofnodes/simple/node/ArithNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/ArithNode.java
@@ -1,8 +1,9 @@
package com.seaofnodes.simple.node;
+import com.seaofnodes.simple.parser.Lexer;
import com.seaofnodes.print.ExprPrinter;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.BAOS;
@@ -13,12 +14,12 @@ import java.util.IdentityHashMap;
public abstract class ArithNode extends Node implements ModeNode {
// Source location for late reported errors
- Parser.Lexer _loc;
+ Lexer _loc;
// Mode is 0 for unknown, 1 for int, 2 for flt
byte _mode;
- public ArithNode( Parser.Lexer loc, Node lhs, Node rhs) { super(null, lhs, rhs); _loc = loc; }
- public ArithNode( Parser.Lexer loc, Node lhs, Node rhs, byte mode) { this(loc,lhs,rhs); _mode=mode; }
+ public ArithNode( Lexer loc, Node lhs, Node rhs) { super(null, lhs, rhs); _loc = loc; }
+ public ArithNode( Lexer loc, Node lhs, Node rhs, byte mode) { this(loc,lhs,rhs); _mode=mode; }
@Override public void packed(BAOS baos, HashMap<String,Integer> strs, HashMap<Type,Integer> types, IdentityHashMap<Node,Integer> anodes) {
baos.packed1(_mode);
}
@@ -65,7 +66,7 @@ public abstract class ArithNode extends Node implements ModeNode {
// Check that a settled mode agrees with all data inputs. Takes the Node
// instead of a varargs list both to avoid allocation and to keep this ready
// to become shared mode-aware Node behavior later.
- static Parser.ParseException nodeErr(Node n, Parser.Lexer loc, String op, byte mode, boolean allowFloat) {
+ static Parser.ParseException nodeErr(Node n, Lexer loc, String op, byte mode, boolean allowFloat) {
Type bad = null;
for( int i=1; i<n.nIns(); i++ ) {
Type t = n.in(i)._type;
@@ -116,10 +117,13 @@ public abstract class ArithNode extends Node implements ModeNode {
if( t2 instanceof TypeConAry ary ) t2 = ary.elem();
if( t1==null || t2==null ) return Type.TOP;
if( t1.isHigh() || t2.isHigh() ) return Type.TOP;
+ // Scalar.BOT is an unknown/uninitialized scalar, not an integer.
+ // Keep that family through arithmetic, including annihilators like *0.
// Without numeric inputs, stay below both possible result families.
// Pointer modes belong to comparisons, not arithmetic.
if( !(t1 instanceof TypeInteger || t1 instanceof TypeFloat) ||
- !(t2 instanceof TypeInteger || t2 instanceof TypeFloat) ) return Type.BOTTOM;
+ !(t2 instanceof TypeInteger || t2 instanceof TypeFloat) )
+ return t1.isa(TypeScalar.BOT) && t2.isa(TypeScalar.BOT) ? TypeScalar.BOT : Type.BOTTOM;
byte mode = _mode==0 ? mode(t1,t2) : _mode;
if( mode==1 ) {
if( t1 instanceof TypeInteger x &&
@@ -127,7 +131,7 @@ public abstract class ArithNode extends Node implements ModeNode {
return x.isConstant() && y.isConstant()
? con(x,y)
: doOp(x,y).makeWide( (byte)Math.max(x._widen,y._widen) );
- return TypeInteger.BOT;
+ return TypeScalar.BOT;
}
// FP handling
if( t1 instanceof TypeFloat f1 && t2 instanceof TypeFloat f2 &&
@@ -181,9 +185,9 @@ public abstract class ArithNode extends Node implements ModeNode {
Node[] ns = new Node[lhs.nIns()];
ns[0] = lhs.in(0);
for( int i=1; i<lhs.nIns(); i++ ) {
- ns[i] = isCon(lhs,rhs,i)
- ? ConstantNode.make(con(lhs.in(i)._type, rhs.in(i)._type)).peephole()
- : copy(lhs.in(i), rhs.in(i)).peephole();
+ // Let the copied operation's compute handle constants
+ // in its actual numeric family, including floats.
+ ns[i] = copy(lhs.in(i), rhs.in(i)).peephole();
}
String label = lhs._label==rhs._label ? lhs._label : lhs._label + rhs._label;
return new PhiNode(label, ns).peephole();
diff --git a/src/main/java/com/seaofnodes/simple/node/BoolNode.java b/src/main/java/com/seaofnodes/simple/node/BoolNode.java
index 73e786e3..203e2266 100644
--- a/src/main/java/com/seaofnodes/simple/node/BoolNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/BoolNode.java
@@ -2,7 +2,7 @@ package com.seaofnodes.simple.node;
import com.seaofnodes.print.ExprPrinter;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.BAOS;
@@ -29,7 +29,7 @@ abstract public class BoolNode extends Node implements ModeNode {
}
@Override
- public TypeInteger compute() {
+ public Type compute() {
Type t1 = in(1)._type;
Type t2 = in(2)._type;
if( t1==null || t2==null )
@@ -37,9 +37,11 @@ abstract public class BoolNode extends Node implements ModeNode {
// Exactly equals?
if( t1.isHigh() || t2.isHigh() )
return (TypeInteger)BOOL.dual();
- if( in(1)==in(2) )
- // LT fails, both EQ and LE succeed
- return this instanceof LT ? FALSE : TRUE;
+ if( t1==Type.BOTTOM || t2==Type.BOTTOM ) return Type.BOTTOM;
+ if( t1==TypeScalar.BOT || t2==TypeScalar.BOT ) return TypeScalar.BOT;
+ if( in(1)==in(2) && (t1 instanceof TypeInteger || t1 instanceof TypeNil) )
+ // Floating x==x is not true when x may be NaN.
+ return this instanceof EQ || this instanceof LE ? TRUE : FALSE;
byte mode = _mode==0 ? ArithNode.mode(t1,t2) : _mode;
if( mode==0 )
return BOOL;
@@ -71,9 +73,7 @@ abstract public class BoolNode extends Node implements ModeNode {
@Override
public Node idealize() {
- // Compare of same
- if( in(1)==in(2) )
- return this instanceof LT ? CodeGen.CODE.ZERO : ConstantNode.make(TRUE);
+ // Constant comparisons, including compare-of-same, belong in compute().
// Can we decide int vs flt?
Type t1 = in(1)._type, t2 = in(2)._type;
if( t1==null || t2==null )
@@ -136,8 +136,8 @@ abstract public class BoolNode extends Node implements ModeNode {
@Override public Tag serialTag() { return Tag.NE; }
public String op() { return "!="; }
TypeInteger doOp(TypeInteger i1, TypeInteger i2) {
- if( i1==i2 && i1.isConstant() ) return TRUE;
- if( i1._max < i2._min || i1._min > i2._max ) return FALSE;
+ if( i1==i2 && i1.isConstant() ) return FALSE;
+ if( i1._max < i2._min || i1._min > i2._max ) return TRUE;
return BOOL;
}
@Override boolean doOp(double lhs, double rhs) { return lhs!=rhs; }
diff --git a/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java b/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java
index 6a902f3c..130b631c 100644
--- a/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/BulkMemPhiNode.java
@@ -258,7 +258,7 @@ public class BulkMemPhiNode extends PhiNode {
MemPhiNode mphi = _findPhi(alias);
if( mphi!=null ) return mphi;
- mphi = new MemPhiNode("$"+alias,alias);
+ mphi = new MemPhiNode(ScopeNode.memName(alias),alias);
mphi.addDef(region());
// Due to cycles, must set before calling peephole
// Splitting preserves the bulk Phi's established escape bound. Starting
diff --git a/src/main/java/com/seaofnodes/simple/node/CFGNode.java b/src/main/java/com/seaofnodes/simple/node/CFGNode.java
index cb406e5b..ba744dc3 100644
--- a/src/main/java/com/seaofnodes/simple/node/CFGNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/CFGNode.java
@@ -1,5 +1,6 @@
package com.seaofnodes.simple.node;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.Ary;
diff --git a/src/main/java/com/seaofnodes/simple/node/CLInitNode.java b/src/main/java/com/seaofnodes/simple/node/CLInitNode.java
index 270c0521..fb762ff3 100644
--- a/src/main/java/com/seaofnodes/simple/node/CLInitNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/CLInitNode.java
@@ -1,6 +1,6 @@
package com.seaofnodes.simple.node;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Lexer;
import com.seaofnodes.print.ExprPrinter;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.codegen.CompUnit;
@@ -14,22 +14,22 @@ import java.util.*;
// also - dead code does not force class init.
//
public class CLInitNode extends CFGNode implements MultiNode {
- public Parser.Lexer _loc; // For error reporting
+ public Lexer _loc; // For error reporting
// Inputs: control, whole memory, class pointer, loaded $init flag
// Results: control, whole memory, known-true flag alias memory, call control.
// For field references, the ptr might be the dst clz ptr or not.
- public CLInitNode(Parser.Lexer loc, Node ctrl, Node mem, Node ptr, Node ready) {
+ public CLInitNode(Lexer loc, Node ctrl, Node mem, Node ptr, Node ready) {
super(ctrl,mem,ptr,ready);
_loc = loc;
}
- CLInitNode(BAOS bais, String[] strs) { super(null,null,null,null); _loc=Parser.Lexer.unpack(bais,strs); }
+ CLInitNode(BAOS bais, String[] strs) { super(null,null,null,null); _loc=Lexer.unpack(bais,strs); }
@Override public Tag serialTag() { return Tag.CLInit; }
@Override public void packed( BAOS baos, HashMap<String,Integer> strs, HashMap<Type,Integer> types, IdentityHashMap<Node, Integer> anodes ) {
- Parser.Lexer.packed(_loc,baos,strs);
+ Lexer.packed(_loc,baos,strs);
}
- @Override public void gather(HashMap<String,Integer> strs) { Parser.Lexer.gather(_loc,strs); }
+ @Override public void gather(HashMap<String,Integer> strs) { Lexer.gather(_loc,strs); }
public CLInitNode(CLInitNode init) {
super(init);
@@ -42,14 +42,14 @@ public class CLInitNode extends CFGNode implements MultiNode {
// The field/alias can be unresolved until forward references are loaded.
public Field flag() {
- return ptr()._type instanceof TypeMemPtr tmp && Parser.startsClzPrefix(tmp._obj._name)
+ return ptr()._type instanceof TypeMemPtr tmp && TypeStruct.startsClzPrefix(tmp._obj._name)
? tmp._obj.field("$init") : null;
}
@Override public String label() { return "CLInit"; }
// Best effort dst compunit, based on ptr - or null
public CompUnit cu() {
- if( !(ptr()._type instanceof TypeMemPtr ptr && Parser.startsClzPrefix(ptr._obj._name)) )
+ if( !(ptr()._type instanceof TypeMemPtr ptr && TypeStruct.startsClzPrefix(ptr._obj._name)) )
return null;
// Convert "class:sys.ary" into "sys/ary" and lookup CompUnit
String fname = ptr._obj._name.substring(6).replace(".","/");
@@ -69,7 +69,7 @@ public class CLInitNode extends CFGNode implements MultiNode {
return ptr()._type==Type.NIL ||
(ptr()._type instanceof TypeMemPtr tmp && tmp._obj!=TypeStruct.BOT &&
!tmp._obj.isHigh() && !tmp._obj._open && !tmp._obj._fref &&
- !Parser.startsClzPrefix(tmp._obj._name));
+ !TypeStruct.startsClzPrefix(tmp._obj._name));
}
public boolean bypass() { return instance() || ready()._type==TypeInteger.TRUE; }
@@ -221,6 +221,6 @@ public class CLInitNode extends CFGNode implements MultiNode {
dep.addDep(init.ptr());
Field flag = init.flag();
if( flag==null || flag._alias!=alias ) return mem;
- return new ProjNode(init,2,Parser.memName(alias)).peephole();
+ return new ProjNode(init,2,ScopeNode.memName(alias)).peephole();
}
}
diff --git a/src/main/java/com/seaofnodes/simple/node/CallEndNode.java b/src/main/java/com/seaofnodes/simple/node/CallEndNode.java
index 2ce2e4d5..b5156c2d 100644
--- a/src/main/java/com/seaofnodes/simple/node/CallEndNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/CallEndNode.java
@@ -2,7 +2,7 @@ package com.seaofnodes.simple.node;
import com.seaofnodes.print.ExprPrinter;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.*;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.BAOS;
diff --git a/src/main/java/com/seaofnodes/simple/node/CallNode.java b/src/main/java/com/seaofnodes/simple/node/CallNode.java
index 23188e84..91d27330 100644
--- a/src/main/java/com/seaofnodes/simple/node/CallNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/CallNode.java
@@ -1,11 +1,13 @@
package com.seaofnodes.simple.node;
+import com.seaofnodes.simple.parser.Lexer;
import com.seaofnodes.print.ExprPrinter;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.codegen.Iter;
import com.seaofnodes.simple.type.Type;
+import com.seaofnodes.simple.type.TypeMemPtr;
import com.seaofnodes.simple.type.TypeFunPtr;
import com.seaofnodes.simple.type.TypeMem;
import com.seaofnodes.simple.type.TypeRPC;
@@ -21,24 +23,27 @@ import java.util.IdentityHashMap;
public class CallNode extends CFGNode {
// Source location for late reported errors
- public final Parser.Lexer _loc;
+ public final Lexer _loc;
- public CallNode(Parser.Lexer loc, Node... nodes) {
+ public CallNode(Lexer loc, Node... nodes) {
super(nodes);
_loc = loc;
CodeGen.CODE._calls.add(this);
+ // Imported targets may be installed after this call is parsed.
+ // Retry linking when Iter drains the parser's worklist.
+ CodeGen.CODE.add(this);
CodeGen.CODE.add(CodeGen.CODE._globalEscapes);
}
public CallNode(CallNode call) { super(call); _loc = call._loc; }
@Override public Tag serialTag() { return Tag.Call; }
public void packed( BAOS baos, HashMap<String,Integer> strs, HashMap<Type,Integer> types, IdentityHashMap<Node, Integer> anodes ) {
baos.packed1(nIns());
- Parser.Lexer.packed(_loc,baos,strs);
+ Lexer.packed(_loc,baos,strs);
}
- @Override public void gather(HashMap<String,Integer> strs) { Parser.Lexer.gather(_loc,strs); }
+ @Override public void gather(HashMap<String,Integer> strs) { Lexer.gather(_loc,strs); }
static Node make( BAOS bais, String[] strs ) {
int nIns = bais.packed1();
- return new CallNode(Parser.Lexer.unpack(bais,strs),new Node[nIns]);
+ return new CallNode(Lexer.unpack(bais,strs),new Node[nIns]);
}
@Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) {
@@ -150,7 +155,8 @@ public class CallNode extends CFGNode {
// Walk the bits and link
for( int fidx = XInt.next(fidxs,0); fidx > 0; fidx = XInt.next(fidxs,fidx) ) {
FunNode fun = CodeGen.CODE.link(fidx);
- if( fun!=null && !fun._folding && !linked(fun) )
+ if( fun==null ) addDepForwards(CodeGen.CODE._start);
+ if( fun!=null && !addDep(fun)._folding && !linked(fun) )
progress = link(fun);
}
}
@@ -240,7 +246,10 @@ public class CallNode extends CFGNode {
// Check for args
for( int i=0; i<tfp.nargs(); i++ )
- if( !arg(i+2)._type.isa(tfp.arg(i)) || !arg(i+2)._type.accessISA(tfp.arg(i)) ) {
+ if( !arg(i+2)._type.isa(tfp.arg(i) instanceof TypeMemPtr p ? p.partialBound() : tfp.arg(i)) || !arg(i+2)._type.accessISA(tfp.arg(i)) ) {
+ if( arg(i+2)._type instanceof TypeMemPtr actual && actual._partial &&
+ tfp.arg(i) instanceof TypeMemPtr formal && !formal._partial )
+ return Parser.error("Cannot pass an incomplete object to a parameter without '^' (ordinary function or constructor)",_loc);
// Constructors return private memory and carry hidden self and
// selfMem arguments ahead of the source-level arguments.
int argno = i - (tfp.ret() instanceof TypeMem ? 2 : 0);
diff --git a/src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java b/src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java
index 284220ea..9e130bb4 100644
--- a/src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/ConFldOffNode.java
@@ -2,7 +2,7 @@ package com.seaofnodes.simple.node;
import com.seaofnodes.print.ExprPrinter;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.type.Type;
import com.seaofnodes.simple.type.TypeInteger;
@@ -21,6 +21,12 @@ public class ConFldOffNode extends ConstantNode {
public TypeStruct _ts; // Struct holding field
public final String _fname; // Field name
public ConFldOffNode( TypeStruct ts, String fname ) { super(TypeInteger.BOT); _ts = ts; _fname = fname; }
+
+ // Field offset for a known type (but unknown offset).
+ public static Node make( TypeStruct ts, String fname ) {
+ return new ConFldOffNode(ts,fname).peephole();
+ }
+
@Override public Tag serialTag() { throw Utils.TODO("should not reach here"); }
@Override public String label() {
@@ -31,20 +37,22 @@ public class ConFldOffNode extends ConstantNode {
return p.p(label());
}
- @Override public Node idealize() {
+ @Override public boolean isConst() { return false; }
+
+ @Override public Type compute() {
// Some array offsets are known early
if( _ts.isAry() ) {
// Length always at offset zero
if( _fname==" len" )
- return CodeGen.CODE.ZERO;
+ return TypeInteger.ZERO;
// The shift depends on the element
if( _fname=="<<" )
- return Node.con(_ts.aryScale());
+ return TypeInteger.constant(_ts.aryScale());
// The base depends on the layout (alignment for large elements may
// involve padding) and MAY depend on the length if I go for
// smaller array lengths.
}
- return null;
+ return _con;
}
// Convert field offset to an integer
diff --git a/src/main/java/com/seaofnodes/simple/node/ConstructNode.java b/src/main/java/com/seaofnodes/simple/node/ConstructNode.java
new file mode 100644
index 00000000..0af37a02
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/node/ConstructNode.java
@@ -0,0 +1,77 @@
+package com.seaofnodes.simple.node;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.parser.Parser;
+import com.seaofnodes.simple.parser.Lexer;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+import com.seaofnodes.print.ExprPrinter;
+
+/** The source new boundary. Own fields have been constructed, but an incomplete
+ * actual argument keeps this result incomplete. Unlike a callee's merged Return,
+ * this transfer describes one allocation site. The partial-only form gives
+ * constructor self and ^ formals their restricted body view even after
+ * inlining substitutes complete actual arguments. Erased after source checks. */
+public class ConstructNode extends Node {
+ public final Lexer _loc;
+ private final boolean _explicit;
+ public final boolean _partial;
+ public ConstructNode(Node value, Ary<Node> args) {
+ super(null,value,null);
+ _loc = null;
+ _explicit = false;
+ _partial = false;
+ for( Node arg : args ) addDef(arg);
+ }
+ // Inside the allocator: keep the final private memory with the result,
+ // so inlining cannot discard the constructor-completion check.
+ public ConstructNode(Node value, Node mem, Lexer loc, boolean explicit) {
+ super(null,value,mem);
+ _loc = loc;
+ _explicit = explicit;
+ _partial = false;
+ }
+ // The constructor body sees self and ^ formals through this restricted view.
+ public ConstructNode(Node value) {
+ super(null,value,null);
+ _loc = null;
+ _explicit = false;
+ _partial = true;
+ }
+ @Override public String label() { return "Construct"; }
+ @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p("construct(").n(in(1)).p(")"); }
+ @Override public Type compute() {
+ Type t = in(1)._type;
+ if( in(2)!=null && in(2)._type.isHigh() ) return Type.TOP;
+ if( !(t instanceof TypeMemPtr ptr) ) return t;
+ boolean partial = _partial;
+ for( int i = 3; i<nIns(); i++ ) partial |= TypeMemPtr.mayBePartial(in(i)._type);
+ return ptr.withPartial(partial);
+ }
+ @Override public Node idealize() {
+ // Restricting an already restricted view is redundant, including after
+ // inlining. A partial Parm type alone can later sharpen to complete.
+ if( _partial && partialView(in(1)) ) return in(1);
+ // Wait for Opto to install closed field types. Afterwards, a proven
+ // identity with initialized fields is good, and can be removed.
+ // Otherwise we're keeping this Node until we error report.
+ return !_partial && CodeGen.CODE._phase.ordinal()>=CodeGen.Phase.Opto.ordinal() && _type==in(1)._type && err()==null ? in(1) : null;
+ }
+ static boolean partialView(Node n) {
+ return n instanceof InitViewNode || n instanceof ConstructNode con && con._partial;
+ }
+ @Override public Parser.ParseException err() {
+ if( in(2)==null || in(2)._type.isHigh() || !(in(1)._type instanceof TypeMemPtr ptr) ) return null;
+ Type mem = in(2)._type;
+ for( Field fld : ptr._obj._fields ) {
+ if( fld._extern ) continue;
+ Field actual = mem instanceof TypeMem tm && tm._t instanceof TypeStruct ts ? ts.field(fld._fname) : null;
+ if( actual==null || actual._t==TypeScalar.BOT || actual._t==Type.BOTTOM )
+ return Parser.error("'"+ptr._obj._name+"' is not fully initialized, field '"+fld._fname+
+ (_explicit ? "' is only partially set in the constructor" : "' needs to be set in a constructor"),_loc);
+ }
+ return null;
+ }
+ @Override public boolean eq(Node n) { return _explicit==((ConstructNode)n)._explicit && _partial==((ConstructNode)n)._partial; }
+ @Override int hash() { return (_explicit ? 1 : 0) | (_partial ? 2 : 0); }
+}
diff --git a/src/main/java/com/seaofnodes/simple/node/ConvertNode.java b/src/main/java/com/seaofnodes/simple/node/ConvertNode.java
index 6e338756..7616932d 100644
--- a/src/main/java/com/seaofnodes/simple/node/ConvertNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/ConvertNode.java
@@ -2,7 +2,7 @@ package com.seaofnodes.simple.node;
import com.seaofnodes.print.ExprPrinter;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.type.*;
import java.util.HashMap;
@@ -39,10 +39,16 @@ public class ConvertNode extends Node {
@Override public Type compute() {
Type src = val()._type;
if( src.isHigh() ) return Type.TOP;
+ if( src==Type.BOTTOM || src==TypeScalar.BOT ) return src;
if( _dst instanceof TypeMemPtr ptr ) {
- // Keep non-nullness before the identity rewrite; shape and access
- // still follow the declaration so compute stays monotone.
- return src instanceof TypeMemPtr actual && actual.notNull() ? ptr.nonZero() : ptr;
+ // Assigning to a nullable field does not make a non-null value
+ // null. Keep this fact during SCCP, before idealize removes us.
+ // Shape/access still follow the declaration: switching between
+ // the entire source and destination types is not monotone.
+ if( src instanceof TypeMemPtr actual && actual.notNull() ) ptr = (TypeMemPtr)ptr.nonZero();
+ ptr = ptr.withPartial(TypeMemPtr.mayBePartial(src));
+ return src instanceof TypeMemPtr actual && actual._partial && actual._obj._name==ptr._obj._name
+ ? ptr.initialized(actual._obj) : ptr;
}
if( src.isa(_dst) ) return src;
return _dst;
@@ -51,13 +57,14 @@ public class ConvertNode extends Node {
@Override public Node idealize() {
Type src = val()._type;
if( src==Type.BOTTOM || src==Type.TOP ) return null;
- if( (src.isa(_dst) && src.accessISA(_dst)) ) return val();
+ Type dst = _dst instanceof TypeMemPtr ptr ? ptr.withPartial(TypeMemPtr.mayBePartial(src)) : _dst;
+ if( (src.isa(dst) && src.accessISA(dst)) ) return val();
// Managed arrays are represented by an object pointer, while C wants
// the address of the first element. The representation cast itself is
// zero-code; the Add skips the array header.
if( _dst==TypeInteger.BOT && src instanceof TypeMemPtr tmp && tmp._obj.isAry() )
- return new AddNode(new PtrToIntNode(val()).peephole(),Parser.off(tmp._obj,"[]"));
+ return new AddNode(new PtrToIntNode(val()).peephole(),ConFldOffNode.make(tmp._obj,"[]"));
// Integer (and nil-as-zero) to floating point.
if( (src instanceof TypeInteger || src==Type.NIL) && _dst instanceof TypeFloat )
@@ -68,10 +75,10 @@ public class ConvertNode extends Node {
return new ConvertNode(_dst,new ToIntegerNode(val()).peephole());
// Narrow integers produce the declared sign/zero extension.
- if( src instanceof TypeInteger && _dst instanceof TypeInteger dst ) {
- if( dst._min==0 )
- return new AndNode(null,val(),con(dst._max));
- int shift = Long.numberOfLeadingZeros(dst._max)-1;
+ if( src instanceof TypeInteger && _dst instanceof TypeInteger ti ) {
+ if( ti._min==0 )
+ return new AndNode(null,val(),con(ti._max));
+ int shift = Long.numberOfLeadingZeros(ti._max)-1;
Node shf = con(shift);
if( shf._type==TypeInteger.ZERO ) return val();
return new SarNode(null,new ShlNode(null,val(),shf.keep()).peephole(),shf.unkeep());
diff --git a/src/main/java/com/seaofnodes/simple/node/CtrlNode.java b/src/main/java/com/seaofnodes/simple/node/CtrlNode.java
index 62140873..277e03dc 100644
--- a/src/main/java/com/seaofnodes/simple/node/CtrlNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/CtrlNode.java
@@ -3,7 +3,7 @@ package com.seaofnodes.simple.node;
import com.seaofnodes.print.ExprPrinter;
import com.seaofnodes.simple.codegen.CodeGen;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.type.Type;
public class CtrlNode extends CFGNode {
diff --git a/src/main/java/com/seaofnodes/simple/node/EscapeNode.java b/src/main/java/com/seaofnodes/simple/node/EscapeNode.java
index f7a88df1..57340609 100644
--- a/src/main/java/com/seaofnodes/simple/node/EscapeNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/EscapeNode.java
@@ -9,19 +9,26 @@ import java.util.HashMap;
import java.util.IdentityHashMap;
/**
- * Merge public and private aliases.
+ * Merge public and private aliases. A borrow orders partial-reader calls
+ * after constructor Stores without adding the private values to escape sets.
* 0 - control
* 1 - private pointer
* 2 - private alias
* 3 - public alias
*/
public class EscapeNode extends TypeNode {
+ public final boolean _borrow;
public EscapeNode(Field fld, Node self, Node priv, Node pub ) {
+ this(fld,self,priv,pub,false);
+ }
+ public EscapeNode(Field fld, Node self, Node priv, Node pub, boolean borrow ) {
super(fld,null,self,priv,pub);
+ _borrow = borrow;
}
public EscapeNode(EscapeNode esc ) {
super(esc); // Copy constructor
+ _borrow = esc._borrow;
// Except we do not want/need self ptr past Opto.
// *NOTE* the weird direct stomp; this is correct because this
// constructor does NOT set the backedges, because it is used to
@@ -38,6 +45,7 @@ public class EscapeNode extends TypeNode {
@Override public Tag serialTag() { return Tag.Escape; }
@Override public void packed( BAOS baos, HashMap<String,Integer> strs, HashMap<Type,Integer> types, IdentityHashMap<Node, Integer> anodes ) {
+ assert !_borrow; // Source ^ interfaces cannot yet be serialized.
baos.packed2(types.get(_con));
}
static Node make( BAOS bais, Type[] types) {
@@ -73,7 +81,14 @@ public class EscapeNode extends TypeNode {
// t is meet of pub & priv. Pub memory can be very weak (e.g. alias 1)
// and we're no worse than the declared field type
- Type t = tpriv.meet(tpub).join(fld._t);
+ Type bound = fld._t;
+ if( _borrow && bound instanceof TypeMemPtr ptr && TypeMemPtr.mayBePartial(tpriv) )
+ bound = ptr.withPartial(true).partialBound();
+ Type t = tpriv.meet(tpub).join(bound);
+ // Lending memory to a local read-only partial argument is an ordering
+ // edge, not publication to unknown callers. Keep the public escape sets.
+ if( _borrow )
+ return TypeMem.make(fld._alias,t,false,false,false,mpub._escFs,mpub._escAs);
int[] fidxes = XInt.meet(mpriv._escFs, mpub._escFs);
int[] aliases = XInt.meet(mpriv._escAs, mpub._escAs);
// Keep escapes from the actual private value before narrowing it to
@@ -112,11 +127,12 @@ public class EscapeNode extends TypeNode {
}
@Override public boolean eq(Node n) {
+ if( _borrow!=((EscapeNode)n)._borrow ) return false;
if( fld()._alias!=((EscapeNode)n).fld()._alias )
return false;
assert _con == ((EscapeNode)n)._con;
return true;
}
- @Override int hash() { return _con.hashCode(); }
+ @Override int hash() { return _con.hashCode() ^ (_borrow ? 1024 : 0); }
}
diff --git a/src/main/java/com/seaofnodes/simple/node/FRefNode.java b/src/main/java/com/seaofnodes/simple/node/FRefNode.java
index 644f1434..583a4300 100644
--- a/src/main/java/com/seaofnodes/simple/node/FRefNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/FRefNode.java
@@ -1,9 +1,10 @@
package com.seaofnodes.simple.node;
+import com.seaofnodes.simple.parser.Lexer;
import com.seaofnodes.print.ExprPrinter;
-import com.seaofnodes.simple.Parser;
-import com.seaofnodes.simple.Var;
+import com.seaofnodes.simple.parser.Parser;
+import com.seaofnodes.simple.parser.Var;
import com.seaofnodes.simple.type.Type;
/**
@@ -22,10 +23,10 @@ import com.seaofnodes.simple.type.Type;
public class FRefNode extends ConstantNode {
public static final Type FREF_TYPE = Type.BOTTOM;
public String _name;
- public final Parser.Lexer _loc; // Source location
+ public final Lexer _loc; // Source location
- public FRefNode( String name, Parser.Lexer loc ) { this(name,loc,FREF_TYPE); }
- public FRefNode( String name, Parser.Lexer loc, Type type ) {
+ public FRefNode( String name, Lexer loc ) { this(name,loc,FREF_TYPE); }
+ public FRefNode( String name, Lexer loc, Type type ) {
super(type);
_name = name;
_loc = loc;
@@ -33,6 +34,8 @@ public class FRefNode extends ConstantNode {
@Override public String label() { return "FRef"+_name; }
+ @Override public Type compute() { return nIns()==1 ? _con : in(1)._type; }
+
@Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) {
return p.p("FRef_").p(_name);
diff --git a/src/main/java/com/seaofnodes/simple/node/FldOffNode.java b/src/main/java/com/seaofnodes/simple/node/FldOffNode.java
index a9f8d692..09e5eac8 100644
--- a/src/main/java/com/seaofnodes/simple/node/FldOffNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/FldOffNode.java
@@ -1,8 +1,9 @@
package com.seaofnodes.simple.node;
+import com.seaofnodes.simple.parser.Lexer;
import com.seaofnodes.print.ExprPrinter;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.SB;
@@ -12,13 +13,13 @@ import java.util.HashMap;
/**
* A field offset. This becomes a constant *after* other optimizations which
* determine field sizes - some fields might be dead, or hold only zeros or
- * small values.
+ * small values. The base is data in slot 1; slot 0 is reserved for control.
*/
public class FldOffNode extends TypeNode {
public final String _fname; // Field name
- public final Parser.Lexer _loc; //
- public FldOffNode( Node base, String fname, Parser.Lexer loc ) { super(TypeInteger.BOT,base); _fname = fname; _loc=loc; }
+ public final Lexer _loc; //
+ public FldOffNode( Node base, String fname, Lexer loc ) { super(TypeInteger.BOT,null,base); _fname = fname; _loc = loc; }
@Override public Tag serialTag() { throw Utils.TODO("should not reach here"); }
@Override public String label() {
@@ -35,7 +36,7 @@ public class FldOffNode extends TypeNode {
@Override public Node idealize() {
// When base() gets typed, this xforms into a ConFldOffNode
- if( in(0)._type instanceof TypeMemPtr tmp )
+ if( in(1)._type instanceof TypeMemPtr tmp )
return new ConFldOffNode(tmp._obj,_fname);
return null;
}
@@ -47,7 +48,7 @@ public class FldOffNode extends TypeNode {
@Override int hash() { return super.hash() * _fname.hashCode(); }
@Override public Parser.ParseException err() {
- if( in(0)._type instanceof TypeMemPtr tmp && tmp._obj._fref )
+ if( in(1)._type instanceof TypeMemPtr tmp && tmp._obj._fref )
return Parser.error("Unknown struct type '"+tmp._obj._name+"'",null);
return null;
}
diff --git a/src/main/java/com/seaofnodes/simple/node/FunNode.java b/src/main/java/com/seaofnodes/simple/node/FunNode.java
index 62915373..8b6b347e 100644
--- a/src/main/java/com/seaofnodes/simple/node/FunNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/FunNode.java
@@ -1,6 +1,7 @@
package com.seaofnodes.simple.node;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Lexer;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.*;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.BAOS;
@@ -27,14 +28,14 @@ public class FunNode extends RegionNode {
public CompUnit _compunit; // Defined in source file
public final boolean _extern; // External function; no code, name only
- private FunNode( Parser.Lexer loc, Node[] nodes, TypeFunPtr sig, String name, CompUnit compunit, boolean ext ) {
+ private FunNode( Lexer loc, Node[] nodes, TypeFunPtr sig, String name, CompUnit compunit, boolean ext ) {
super(loc,nodes);
_name = name;
_sig = sig;
_extern = ext;
_compunit = compunit;
}
- public FunNode( Parser.Lexer loc, TypeFunPtr sig, String name, CompUnit compunit, Node... nodes ) { this(loc,nodes,sig,name,compunit,false); }
+ public FunNode( Lexer loc, TypeFunPtr sig, String name, CompUnit compunit, Node... nodes ) { this(loc,nodes,sig,name,compunit,false); }
public FunNode( FunNode fun ) {
super( fun, fun._loc );
_sig = fun.sig();
@@ -101,8 +102,10 @@ public class FunNode extends RegionNode {
// uniform call syntax, including callbacks forwarded by another class.
public Type defaultArg(int idx) {
Type t = sig().arg(idx);
+ if( t instanceof TypeMemPtr ptr && ptr._partial &&
+ !(idx==0 && sig().nargs()>1 && sig().arg(1) instanceof TypeMem) ) return ptr.partialBound();
if( idx!=0 || t!=TypePtr.PTR ) return t;
- TypeStruct clz = (TypeStruct)Parser.TYPES.get(Parser.addClzPrefix(_compunit._cname));
+ TypeStruct clz = (TypeStruct)Parser.TYPES.get(TypeStruct.addClzPrefix(_compunit._cname));
assert clz!=null;
return TypeMemPtr.make((byte)2,clz,true);
}
@@ -180,7 +183,7 @@ public class FunNode extends RegionNode {
}
// Upgrade inferred or user-written return type to actual
- if( _ret!=null && _ret._type instanceof TypeTuple tt &&
+ if( _ret!=null && addDep(_ret)._type instanceof TypeTuple tt &&
tt.ret() != _sig.ret() && tt.ret().isa(_sig.ret()) ) {
setSig(_sig.makeFrom(tt.ret()));
return this;
@@ -190,12 +193,19 @@ public class FunNode extends RegionNode {
// - Must be past pessimistic Iter, which discovers and links calls
// through class-field loads and other initially imprecise values.
// - FIDX is not in the escape set
- // - Not called anywhere else; post Opto called-not-escaped functions need to still be hooked to start
+ // Concrete callers keep the body alive; FinalizeIR retains surviving
+ // functions for emission without giving them unknown callers.
if( !isRoot() && in(1) instanceof StartCUNode start ) {
boolean preOpto = CodeGen.CODE._phase == null ||
CodeGen.CODE._phase.ordinal() < CodeGen.Phase.Opto.ordinal();
- if( !preOpto && !start.escapedFIDX(_sig.fidx()) && nIns()<=2 && _ret!=null ) {
- _compunit._stop.delDef(_compunit._stop._inputs.find(ret()));
+ if( !preOpto && !start.escapedFIDX(_sig.fidx()) && _ret!=null ) {
+ // CLInit has a call edge but no Return edge. Keep a called
+ // body's Return rooted even after dropping unknown callers.
+ if( !isClz() || nIns()<=2 ) {
+ StopCUNode stop = _compunit._stop;
+ CODE.add(stop).delDef(stop._inputs.find(ret()));
+ CODE.addAll(stop._outputs); // The outer Stop can drop an empty unit.
+ }
return removeDeadPath(1);
} else {
addDep(start);
diff --git a/src/main/java/com/seaofnodes/simple/node/InitViewNode.java b/src/main/java/com/seaofnodes/simple/node/InitViewNode.java
new file mode 100644
index 00000000..46988a99
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/node/InitViewNode.java
@@ -0,0 +1,30 @@
+package com.seaofnodes.simple.node;
+
+import com.seaofnodes.print.ExprPrinter;
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.type.*;
+
+/** A program-point view of self. Memory proves which slots may be read;
+ * their values still come from ordinary Loads. Erased after TypeCheck. */
+public class InitViewNode extends TypeNode {
+ public InitViewNode(TypeMemPtr declared, Node self, Node mem) { super(declared,null,self,mem); }
+ @Override public String label() { return "InitView"; }
+ @Override protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) { return p.p("initView(").n(in(1)).p(")"); }
+ @Override public Type compute() {
+ Type ptr = in(1)._type, mem = in(2)._type;
+ if( ptr.isHigh() || mem.isHigh() ) return Type.TOP;
+ if( !(ptr instanceof TypeMemPtr p) ) return ptr;
+ TypeStruct state = mem instanceof TypeMem m && m._one && m._t instanceof TypeStruct ts
+ ? ts : p._obj.makeInit();
+ // An earlier alias can have fewer initialized slots. The declaration
+ // supplies field types; this program point's memory supplies the proof.
+ return p.makeFrom(((TypeMemPtr)_con)._obj).withPartial(true).initialized(state);
+ }
+ @Override public Node idealize() {
+ // Closed declarations and settled memory must prove this view redundant.
+ // Keep a structural partial restriction: a Parm's current partial type
+ // alone is not enough, since inlining can substitute a complete caller.
+ return CodeGen.CODE._phase.ordinal()>=CodeGen.Phase.Opto.ordinal() &&
+ _type==in(1)._type && ConstructNode.partialView(in(1)) ? in(1) : null;
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/node/LoadNode.java b/src/main/java/com/seaofnodes/simple/node/LoadNode.java
index 22836d90..85ab29f9 100644
--- a/src/main/java/com/seaofnodes/simple/node/LoadNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/LoadNode.java
@@ -1,8 +1,9 @@
package com.seaofnodes.simple.node;
+import com.seaofnodes.simple.parser.Lexer;
import com.seaofnodes.print.ExprPrinter;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.codegen.GlobalBits;
import com.seaofnodes.simple.type.*;
@@ -24,7 +25,7 @@ public class LoadNode extends MemOpNode {
* @param ptr The ptr to the struct base from where we load a field
* @param off The offset inside the struct base
*/
- public LoadNode(Parser.Lexer loc, String name, int alias, Node ctrl, Node mem, Node ptr, Node off) {
+ public LoadNode(Lexer loc, String name, int alias, Node ctrl, Node mem, Node ptr, Node off) {
super(loc, name, alias, true, Type.BOTTOM, ctrl, mem, ptr, off);
}
LoadNode( BAOS bais, String[] strs, Type[] types, GlobalBits fileAliases, GlobalBits aliases ) {
@@ -85,14 +86,18 @@ public class LoadNode extends MemOpNode {
if( pfld == null )
return tmp.isHigh() || tmp._obj.isHigh() ? TypeScalar.TOP : TypeScalar.BOT;
+ if( tmp._partial && (!mem._one || mem._clz) && pfld._t==TypeScalar.BOT ) return TypeScalar.BOT;
+
Type t = pfld._t;
boolean readonly = t instanceof TypeMemPtr view && view._ro;
// Load member of constant array
if( t instanceof TypeConAry ary )
t = ary.elem(); // TODO: if offset is known, can peek the constant
- if( mem._alias==_alias )
+ if( mem._one && !mem._clz && !tmp._obj.isAry() ) {
+ Field actual = mem._t instanceof TypeStruct ts ? ts.field(_name) : null;
+ t = mem._alias==_alias ? mem._t : actual==null ? TypeScalar.BOT : actual._t;
+ } else if( mem._alias==_alias )
t = t.join(mem._t);
-
if( !mem._one ) t = mem.outside(t);
// Store forwarding is also a value fact during SCCP. Waiting for the
// later rewrite can turn a known non-null stored pointer into a
@@ -105,6 +110,12 @@ public class LoadNode extends MemOpNode {
// generic declared field type must not cast this information away.
if( ptr0.isFinal() || readonly )
t = t.makeRO();
+ // A completed receiver gives a completed view of its reachable fields.
+ // Through self, keep object references restricted until the root new.
+ if( t instanceof TypeMemPtr value ) {
+ value = value.withPartial(tmp._partial && !value._obj.isAry());
+ t = value._partial ? value.partialBound() : value;
+ }
return scalar(t);
}
@@ -117,6 +128,7 @@ public class LoadNode extends MemOpNode {
@Override
public Node idealize() {
+ if( _type==TypeScalar.BOT ) return null;
Node mem = mem();
Node ptr = ptr();
@@ -176,7 +188,9 @@ public class LoadNode extends MemOpNode {
// if( pred ) ptr.x = e0; val = pred ? e0
// else ptr.x = e1; : e1;
// val = ptr.x; ptr.x = val;
- if( mem() instanceof MemPhiNode memphi && memphi.region()._type == Type.CONTROL && memphi.nIns()== 3 &&
+ if( mem() instanceof PhiNode memphi && !(memphi instanceof ParmNode) &&
+ (memphi instanceof MemPhiNode || (memphi._type instanceof TypeMem tm && tm._one)) &&
+ memphi.region()._type == Type.CONTROL && memphi.nIns()== 3 &&
// Retain control on both copies only if it dominates both paths.
// The Region itself, or a later range check, cannot move to its inputs.
(in(0)==null || cfg0().domLCA(memphi.region().idom(this),this)==cfg0()) &&
@@ -210,6 +224,9 @@ public class LoadNode extends MemOpNode {
// Load a flavored zero from a New
private Node zero(NewNode nnn) {
+ // Object fields acquire values from their initializer's Stores.
+ // Only arrays expose calloc's zero-fill as a language-level default.
+ if( !nnn._ts.isAry() ) return new ConstantNode(TypeScalar.BOT).peephole();
Type zero = declaredType().makeZero();
assert zero.isa(_type); // Catch an uninitialized non-null field
return castRO(new ConstantNode(zero).peephole());
@@ -237,8 +254,8 @@ public class LoadNode extends MemOpNode {
if( st._alias==1 ) return null;
addDep(st.ptr());
if( alias==st._alias && (ptr==address(st.ptr()) || ptr==address(st.nnptr())) &&
- ((!_name.equals("[]") && _name.equals(st._name)) || off()==st.off() ||
- (addDep(off())._type.isConstant() && off()._type==addDep(st.off())._type)) ) {
+ (!_name.equals("[]") && _name.equals(st._name) || off()==st.off() ||
+ addDep(off())._type.isConstant() && off()._type==addDep(st.off())._type) ) {
// Forwarding must preserve the pointer type already seen
// by users: completion, read-only view, and recursive shape.
// Open declarations can disagree until Opto upgrades them.
@@ -273,7 +290,8 @@ public class LoadNode extends MemOpNode {
if( address(esc.self())==ptr ) { mem=esc.priv(); break; }
if( neverAlias(ptr,address(esc.self())) ) { mem=esc.pub(); break; }
return null;
- case MemPhiNode phi:
+ case PhiNode phi:
+ if( phi instanceof ParmNode || (!(phi instanceof MemPhiNode) && !(phi._type instanceof TypeMem tm && tm._one)) ) return null;
addDep(phi.region());
// Only the original loop closes a successful cycle. Another
// loop or an unfinished merge needs a separate proof.
@@ -300,7 +318,7 @@ public class LoadNode extends MemOpNode {
Node win=find(mem,ptr,stop,new BitSet());
if( win!=null ) {
if( win==stop ) return value;
- if( win instanceof MemPhiNode mp ) {
+ if( win instanceof PhiNode mp ) {
PhiNode phi=new PhiNode(_name,mp.region()).keep();
for( int i=1; i<mp.nIns(); i++ ) phi.addDef(load(mp.in(i),ptr,stop,value));
return CodeGen.CODE.add(phi.unkeep().peephole());
@@ -324,7 +342,7 @@ public class LoadNode extends MemOpNode {
if( ptr instanceof ConvertNode conv && addDep(conv.val())._type instanceof TypeMemPtr src &&
src.isa(addDep(conv)._type) && src.accessISA(conv._type) )
ptr = conv.val();
- while( ptr instanceof ReadOnlyNode ) {
+ while( ptr instanceof InitViewNode || ptr instanceof ReadOnlyNode || ptr instanceof ConstructNode con && con._partial ) {
addDep(ptr);
ptr = ptr.in(1);
}
@@ -357,6 +375,15 @@ public class LoadNode extends MemOpNode {
return false;
}
+ @Override public Parser.ParseException err() {
+ Parser.ParseException err = super.err();
+ if( err!=null ) return err;
+ if( (in(0)==null || !in(0)._type.isHigh()) && _type==TypeScalar.BOT &&
+ mem()._type instanceof TypeMem mem && ((mem._one && !mem._clz) || (ptr()._type instanceof TypeMemPtr p && p._partial)) )
+ return Parser.error("Field '"+_name+"' might not be initialized",_loc);
+ return null;
+ }
+
// Read-Only is a deep property, and cannot be cast-away
private Node castRO(Node rez) {
if( (ptr()._type.isFinal() || declaredType().isFinal()) && !rez._type.isFinal() )
diff --git a/src/main/java/com/seaofnodes/simple/node/LoopNode.java b/src/main/java/com/seaofnodes/simple/node/LoopNode.java
index 45f7a6fa..c4d55335 100644
--- a/src/main/java/com/seaofnodes/simple/node/LoopNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/LoopNode.java
@@ -1,6 +1,7 @@
package com.seaofnodes.simple.node;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Lexer;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.type.Type;
import com.seaofnodes.simple.type.TypeMem;
@@ -10,7 +11,7 @@ import java.util.HashMap;
import java.util.IdentityHashMap;
public class LoopNode extends RegionNode {
- public LoopNode( Parser.Lexer loc, Node xctrl, Node entry ) { super(loc,xctrl,entry,null); }
+ public LoopNode( Lexer loc, Node xctrl, Node entry ) { super(loc,xctrl,entry,null); }
public LoopNode( LoopNode loop ) { super(loop); }
@Override public Tag serialTag() { return Tag.Loop; }
public void packed( BAOS baos, HashMap<String,Integer> strs, HashMap<Type,Integer> types, IdentityHashMap<Node, Integer> anodes ) { }
diff --git a/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java b/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java
index dfe8794d..aa3f6d57 100644
--- a/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/MemMergeNode.java
@@ -1,5 +1,6 @@
package com.seaofnodes.simple.node;
+import com.seaofnodes.simple.parser.Var;
import com.seaofnodes.print.ExprPrinter;
import com.seaofnodes.simple.*;
diff --git a/src/main/java/com/seaofnodes/simple/node/MemOpNode.java b/src/main/java/com/seaofnodes/simple/node/MemOpNode.java
index bfbd46c3..35183380 100644
--- a/src/main/java/com/seaofnodes/simple/node/MemOpNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/MemOpNode.java
@@ -1,8 +1,9 @@
package com.seaofnodes.simple.node;
+import com.seaofnodes.simple.parser.Lexer;
import com.seaofnodes.print.ExprPrinter;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.codegen.GlobalBits;
import com.seaofnodes.simple.type.*;
@@ -40,16 +41,16 @@ public abstract class MemOpNode extends TypeNode {
// Field name.
public final String _name;
// Source location for late reported errors
- public final Parser.Lexer _loc;
+ public final Lexer _loc;
- public MemOpNode(Parser.Lexer loc, String name, int alias, boolean isLoad, Type decl, Node ctrl, Node mem, Node ptr, Node off) {
+ public MemOpNode(Lexer loc, String name, int alias, boolean isLoad, Type decl, Node ctrl, Node mem, Node ptr, Node off) {
super(decl, ctrl, mem, ptr, off);
_name = name;
_alias = alias;
_loc = loc;
_isLoad = isLoad;
}
- public MemOpNode(Parser.Lexer loc, String name, int alias, boolean isLoad, Type decl, Node ctrl, Node mem, Node ptr, Node off, Node value) {
+ public MemOpNode(Lexer loc, String name, int alias, boolean isLoad, Type decl, Node ctrl, Node mem, Node ptr, Node off, Node value) {
this(loc, name, alias, isLoad, decl, ctrl, mem, ptr, off);
addDef(value);
}
@@ -122,6 +123,9 @@ public abstract class MemOpNode extends TypeNode {
// Already an error, but better error messages come from elsewhere
if( ptr == Type.BOTTOM ) return null;
if( ptr.isHigh() ) return null; // Assume it will fall to not-null
+ if( !_isLoad && ptr instanceof TypeMemPtr p && p._partial &&
+ !(mem()._type instanceof TypeMem m && m._one && !m._clz) )
+ return Parser.error("Cannot access field '"+_name+"' through an incomplete object",_loc);
// Better be a not-nil TMP
if( !(ptr instanceof TypeMemPtr tmp && tmp.notNull()) )
return Parser.error( "Might be null accessing '" + _name + "'",_loc);
diff --git a/src/main/java/com/seaofnodes/simple/node/MinusNode.java b/src/main/java/com/seaofnodes/simple/node/MinusNode.java
index 9c99fc6b..63d413d0 100644
--- a/src/main/java/com/seaofnodes/simple/node/MinusNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/MinusNode.java
@@ -1,7 +1,7 @@
package com.seaofnodes.simple.node;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.BAOS;
import com.seaofnodes.simple.util.Utils;
@@ -47,12 +47,12 @@ public class MinusNode extends Node implements ModeNode {
return TypeInteger.BOT;
}
if( t instanceof TypeFloat f ) {
- if( _mode==1 ) return TypeInteger.BOT;
+ if( _mode==1 ) return TypeScalar.BOT;
return f.isConstant() ? TypeFloat.constant(-f.value()) : f;
}
// No numeric family is known. This must be below both integer and
// float results, in either analysis direction and with any mode.
- return Type.BOTTOM;
+ return t.isa(TypeScalar.BOT) ? TypeScalar.BOT : Type.BOTTOM;
}
@Override
diff --git a/src/main/java/com/seaofnodes/simple/node/MulNode.java b/src/main/java/com/seaofnodes/simple/node/MulNode.java
index bb99cad5..87141456 100644
--- a/src/main/java/com/seaofnodes/simple/node/MulNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/MulNode.java
@@ -16,6 +16,7 @@ public class MulNode extends ArithNode {
@Override long doOp( long x, long y ) { return x * y; }
@Override double doOp(double x, double y ) { return x * y; }
@Override TypeInteger doOp(TypeInteger x, TypeInteger y) {
+ if( x==TypeInteger.ZERO || y==TypeInteger.ZERO ) return TypeInteger.ZERO;
return TypeInteger.BOT;
}
@@ -26,6 +27,11 @@ public class MulNode extends ArithNode {
Type t1 = lhs._type;
Type t2 = rhs._type;
+ // Integer strength reductions require two integer operands. In
+ // particular Scalar.BOT * 0 must retain its unresolved input.
+ if( _mode==2 || !(t1 instanceof TypeInteger && t2 instanceof TypeInteger) )
+ return super.idealize();
+
// Move constants to RHS: con*arg becomes arg*con
if ( t1.isConstant() && !t2.isConstant() )
return swap12();
@@ -36,7 +42,7 @@ public class MulNode extends ArithNode {
// canonicalize to (x*1)
long c = i.value();
if( c==1 ) return lhs._type.isa(compute()) ? lhs : super.idealize();
- if( c==0 ) return CodeGen.CODE.ZERO;
+ // Zero is handled by compute(), before idealize().
// Mul by a power of 2, +/-1. Bit patterns more complex than this
// are unlikely to win on an X86 vs the normal "imul", and so
// become machine-specific.
diff --git a/src/main/java/com/seaofnodes/simple/node/NeverNode.java b/src/main/java/com/seaofnodes/simple/node/NeverNode.java
index ba1dab8c..f7b5a58a 100644
--- a/src/main/java/com/seaofnodes/simple/node/NeverNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/NeverNode.java
@@ -2,7 +2,7 @@ package com.seaofnodes.simple.node;
import com.seaofnodes.print.ExprPrinter;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.type.TypeInteger;
import com.seaofnodes.simple.type.Type;
import com.seaofnodes.simple.type.TypeTuple;
diff --git a/src/main/java/com/seaofnodes/simple/node/NewNode.java b/src/main/java/com/seaofnodes/simple/node/NewNode.java
index fe80f690..96a237e1 100644
--- a/src/main/java/com/seaofnodes/simple/node/NewNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/NewNode.java
@@ -1,7 +1,7 @@
package com.seaofnodes.simple.node;
import com.seaofnodes.print.ExprPrinter;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.*;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.BAOS;
@@ -44,9 +44,9 @@ public class NewNode extends Node implements MultiNode {
TypeStruct ts = _ts.isAry() ? _ts : _ts.makeInit();
return TypeTuple.make(TypeMemPtr.make(_ts),
// Arrays get pre-zeroed by constructor.
- // Objects start with default-zero memory for
- // fields that can be zero-initialized. This is
- // still private memory, so no aliases escape yet.
+ // Object fields start at Scalar.BOT; declaration
+ // defaults are explicit Stores. This is private
+ // memory, so no aliases escape yet.
TypeMem.make(1,ts,true,false,false,null,null));
}
diff --git a/src/main/java/com/seaofnodes/simple/node/Node.java b/src/main/java/com/seaofnodes/simple/node/Node.java
index 515b29f4..2815dfba 100644
--- a/src/main/java/com/seaofnodes/simple/node/Node.java
+++ b/src/main/java/com/seaofnodes/simple/node/Node.java
@@ -2,8 +2,8 @@ package com.seaofnodes.simple.node;
import com.seaofnodes.print.BaseNode;
-import com.seaofnodes.simple.IterPeeps;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.codegen.IterPeeps;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.codegen.GlobalBits;
import com.seaofnodes.simple.print.IRPrinter;
@@ -525,7 +525,7 @@ public abstract class Node extends BaseNode<Node> implements Cloneable {
}
@SuppressWarnings("unchecked")
- public <N extends Node> N init() { _type = compute(); return (N)this; }
+ public <N extends Node> N init() { _type = compute(); CODE.add(this); return (N)this; }
/**
* This function rewrites the current Node into a more "idealized" form.
@@ -753,14 +753,16 @@ public abstract class Node extends BaseNode<Node> implements Cloneable {
// If a Type upgrade happens, put on worklist
boolean _upgradeType( HashMap<String,Type> TYPES ) { return false; }
- public final void upgradeType( HashMap<String,Type> TYPES ) {
+ public final boolean upgradeType( HashMap<String,Type> TYPES ) {
Type old = _type; _type = _type.upgradeType(TYPES);
boolean progress = _upgradeType(TYPES);
if( progress || old != _type ) {
CODE.add(this);
CODE.addAll(_outputs);
if( _deps!=null ) CODE.addAll(_deps);
+ return true;
}
+ return false;
}
/**
diff --git a/src/main/java/com/seaofnodes/simple/node/NotNode.java b/src/main/java/com/seaofnodes/simple/node/NotNode.java
index 2328c287..c3a795c9 100644
--- a/src/main/java/com/seaofnodes/simple/node/NotNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/NotNode.java
@@ -11,9 +11,10 @@ public class NotNode extends Node {
@Override protected String format() { return "(!%1)"; }
@Override
- public TypeInteger compute() {
+ public Type compute() {
Type t0 = in(1)._type;
if( t0.isHigh() ) return (TypeInteger)TypeInteger.BOOL.dual();
+ if( t0==Type.BOTTOM || t0==TypeScalar.BOT ) return t0;
if( t0 == Type.NIL || t0 == TypeInteger.ZERO ) return TypeInteger.TRUE;
if( t0 instanceof TypeNil tn && tn.notNull() ) return TypeInteger.FALSE;
if( t0 instanceof TypeInteger i && (i._min > 0 || i._max < 0) ) return TypeInteger.FALSE;
diff --git a/src/main/java/com/seaofnodes/simple/node/OrNode.java b/src/main/java/com/seaofnodes/simple/node/OrNode.java
index 9d50d1a9..c7dcc75e 100644
--- a/src/main/java/com/seaofnodes/simple/node/OrNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/OrNode.java
@@ -1,11 +1,12 @@
package com.seaofnodes.simple.node;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Lexer;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.type.Type;
import com.seaofnodes.simple.type.TypeInteger;
public class OrNode extends ArithNode {
- public OrNode( Parser.Lexer loc, Node lhs, Node rhs) { super(loc,lhs, rhs, (byte)1); }
+ public OrNode( Lexer loc, Node lhs, Node rhs) { super(loc,lhs, rhs, (byte)1); }
@Override public Tag serialTag() { return Tag.Or; }
@Override public String label() { return "Or"; }
diff --git a/src/main/java/com/seaofnodes/simple/node/ParmNode.java b/src/main/java/com/seaofnodes/simple/node/ParmNode.java
index 99b6ad11..aea402f6 100644
--- a/src/main/java/com/seaofnodes/simple/node/ParmNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/ParmNode.java
@@ -4,6 +4,7 @@ import com.seaofnodes.print.ExprPrinter;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.codegen.Serialize;
+import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.util.BAOS;
import java.util.HashMap;
@@ -19,7 +20,7 @@ public class ParmNode extends PhiNode {
super(label, inputs);
_idx = idx;
_declaredType = declaredType;
- _type = declaredType;
+ _type = declaredBound();
}
public ParmNode(ParmNode parm) { super(parm, parm._label ); _idx = parm._idx; _declaredType = parm._declaredType; }
@Override public Tag serialTag() { return Tag.Parm; }
@@ -48,6 +49,12 @@ public class ParmNode extends PhiNode {
public FunNode fun() { return (FunNode)in(0); }
+ // The hidden constructor receiver has private memory for checking reads.
+ // Only borrowed source parameters need an initially empty field promise.
+ private Type declaredBound() {
+ return !(_idx==2 && "self".equals(_label)) && _declaredType instanceof TypeMemPtr ptr ? ptr.partialBound() : _declaredType;
+ }
+
@Override
protected ExprPrinter<Node> _print1(ExprPrinter<Node> p) {
if( fun()._name!=null && fun()._name.endsWith("<clinit>") && _label.equals("arg") )
@@ -67,7 +74,7 @@ public class ParmNode extends PhiNode {
return region()._type==Type.XCONTROL || region()._type==Type.TOP ? (_declaredType instanceof TypeMem ? TypeMem.TOP : Type.TOP) : _type;
// During parsing Phis have to be computed type pessimistically.
if( r.inProgress() || in(nIns()-1)==null )
- return _declaredType;
+ return declaredBound();
// Set type to local top of the starting type
Type t = Type.TOP;
for( int i = 1; i < nIns(); i++ ) {
@@ -92,7 +99,7 @@ public class ParmNode extends PhiNode {
// widening this Parm alone cannot stop f(x-1)+1 from growing its result.
if( t instanceof TypeInteger ti && !ti.isHigh() && !ti.isConstant() )
t = ti.same_but_slightly_wider_than();
- t = t.join(_declaredType);
+ t = t.join(declaredBound());
return t==Type.TOP && _declaredType instanceof TypeMem ? TypeMem.TOP : t;
}
diff --git a/src/main/java/com/seaofnodes/simple/node/PhiNode.java b/src/main/java/com/seaofnodes/simple/node/PhiNode.java
index 3cf6f854..f45c7325 100644
--- a/src/main/java/com/seaofnodes/simple/node/PhiNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/PhiNode.java
@@ -1,5 +1,6 @@
package com.seaofnodes.simple.node;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.print.ExprPrinter;
import com.seaofnodes.simple.*;
@@ -18,6 +19,13 @@ public class PhiNode extends Node {
_label = label;
}
public static PhiNode make(String label, Type minType, Node... inputs) {
+ if( minType instanceof TypeMem mem && mem._one ) {
+ // One object's memory travels as one value. Its unfinished loop
+ // Phi still belongs to the private-memory family.
+ PhiNode phi = new PhiNode(label,inputs);
+ phi._type = TypeMem.makePrivate(Type.BOTTOM);
+ return phi;
+ }
return minType instanceof TypeMem mem
? mem._alias==1
? new BulkMemPhiNode(label,inputs)
@@ -66,7 +74,7 @@ public class PhiNode extends Node {
return Type.TOP;
// During parsing Phis have to be computed type pessimistically.
if( r.inProgress() || in(nIns()-1)==null )
- return Type.BOTTOM;
+ return _type instanceof TypeMem mem && mem._one ? TypeMem.makePrivate(Type.BOTTOM) : Type.BOTTOM;
// Set type to local top of the starting type
Type t = Type.TOP;
for( int i = 1; i < nIns(); i++ ) {
@@ -93,7 +101,7 @@ public class PhiNode extends Node {
return ti.same_but_slightly_wider_than();
}
- return t;
+ return r instanceof LoopNode && t instanceof TypeMem mem ? mem.widen() : t;
}
@Override
@@ -192,7 +200,7 @@ public class PhiNode extends Node {
private Node drop_same_op() {
Node op = in(1), cp = op.copyEmpty();
cp._type = null;
- cp.addDef(null);
+ cp._inputs.add(null);
for( int j=1; j<op.nIns(); j++ ) {
Node x = op.in(j);
boolean different = false;
@@ -207,27 +215,54 @@ public class PhiNode extends Node {
t = t.meet(ins[i]._type);
}
// Do not introduce an operand Phi mixing incompatible type families.
- if( t==Type.BOTTOM ) {
+ if( t==Type.BOTTOM || t==TypeScalar.BOT ) {
for( int i=1; i<ins.length; i++ ) addDep(ins[i]);
- cp.kill();
+ discardTrial(cp);
return null;
}
PhiNode phi = j==1 && op instanceof MemOpNode mem
- ? new MemPhiNode(_label,mem._alias,ins)
- : PhiNode.make(_label,t,ins);
- x = phi.peephole();
+ ? new MemPhiNode(_label,mem._alias)
+ : PhiNode.make(_label,t);
+ // Compute the trial without changing any existing use lists.
+ // Rejecting and killing a wired trial would wake our own
+ // dependencies and retry the same unsuccessful fold forever.
+ for( Node in : ins ) phi._inputs.add(in);
+ phi._type = phi.compute();
+ x = phi;
}
- cp.addDef(x);
+ cp._inputs.add(x);
}
// Factoring must not widen the result (e.g. correlated And operands).
- if( cp.compute().isa(compute()) ) return cp;
+ if( cp.compute().isa(compute()) ) {
+ for( int j=1; j<cp.nIns(); j++ ) {
+ Node x = cp.in(j);
+ if( x instanceof PhiNode && x.isUnused() ) {
+ for( Node in : x._inputs )
+ if( in!=null ) in.addUse(x);
+ x = x.peephole();
+ cp._inputs.set(j,x);
+ }
+ if( x!=null ) x.addUse(cp);
+ }
+ return cp;
+ }
for( int i=1; i<nIns(); i++ )
for( int j=1; j<in(i).nIns(); j++ )
if( in(i).in(j)!=null ) addDep(in(i).in(j));
- cp.kill();
+ discardTrial(cp);
return null;
}
+ // Trial inputs have no matching use edges yet, so ordinary kill is wrong.
+ private static void discardTrial(Node cp) {
+ for( Node x : cp._inputs )
+ if( x instanceof PhiNode && x.isUnused() ) {
+ x._inputs.clear();
+ x._type = null;
+ }
+ cp._inputs.clear();
+ }
+
/**
* If only single unique input, return it
*/
diff --git a/src/main/java/com/seaofnodes/simple/node/PtrToIntNode.java b/src/main/java/com/seaofnodes/simple/node/PtrToIntNode.java
index d3d38060..8c2bc069 100644
--- a/src/main/java/com/seaofnodes/simple/node/PtrToIntNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/PtrToIntNode.java
@@ -1,10 +1,11 @@
package com.seaofnodes.simple.node;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.type.Type;
import com.seaofnodes.simple.type.TypeInteger;
import com.seaofnodes.simple.type.TypeMemPtr;
+import com.seaofnodes.simple.type.TypeScalar;
/**
@@ -22,6 +23,7 @@ public class PtrToIntNode extends Node {
@Override protected String format() { return "(i64*)%1"; }
@Override public Type compute() {
+ if( in(1)._type==Type.BOTTOM || in(1)._type==TypeScalar.BOT ) return in(1)._type;
return in(1)._type.isHigh() ? TypeInteger.TOP : TypeInteger.BOT;
}
diff --git a/src/main/java/com/seaofnodes/simple/node/RegionNode.java b/src/main/java/com/seaofnodes/simple/node/RegionNode.java
index 8883ba8e..eee1c150 100644
--- a/src/main/java/com/seaofnodes/simple/node/RegionNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/RegionNode.java
@@ -1,8 +1,9 @@
package com.seaofnodes.simple.node;
+import com.seaofnodes.simple.parser.Lexer;
import com.seaofnodes.print.ExprPrinter;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.type.Type;
import com.seaofnodes.simple.util.BAOS;
@@ -10,10 +11,10 @@ import java.util.*;
public class RegionNode extends CFGNode {
// Source location for late discovered errors
- public Parser.Lexer _loc;
+ public Lexer _loc;
- public RegionNode(Parser.Lexer loc, Node... nodes) { super(nodes); _loc = loc; }
- public RegionNode(RegionNode r, Parser.Lexer loc) { super(r); _loc = loc; }
+ public RegionNode(Lexer loc, Node... nodes) { super(nodes); _loc = loc; }
+ public RegionNode(RegionNode r, Lexer loc) { super(r); _loc = loc; }
public RegionNode(RegionNode r) { super(r); if( r!=null ) _loc = r._loc; }
@Override public Tag serialTag() { return Tag.Region; }
public void packed( BAOS baos, HashMap<String,Integer> strs, HashMap<Type,Integer> types, IdentityHashMap<Node, Integer> anodes ) { baos.packed1(nIns()); }
@@ -141,7 +142,7 @@ public class RegionNode extends CFGNode {
{ addDep(use); if( data!=null ) addDep(data); return true; }
}
} else
- { addDep(use); return true; } // Control user
+ { if( use!=null ) addDep(use); return true; } // Control user, including an external keep
}
return false;
}
diff --git a/src/main/java/com/seaofnodes/simple/node/ReturnNode.java b/src/main/java/com/seaofnodes/simple/node/ReturnNode.java
index 311a7ea4..f2972c91 100644
--- a/src/main/java/com/seaofnodes/simple/node/ReturnNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/ReturnNode.java
@@ -2,7 +2,7 @@ package com.seaofnodes.simple.node;
import com.seaofnodes.print.ExprPrinter;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.codegen.Encoding;
import com.seaofnodes.simple.codegen.RegMask;
@@ -77,22 +77,6 @@ public class ReturnNode extends CFGNode {
if( ctrl()._type == Type.CONTROL &&
expr()._type == TypeScalar.BOT )
return Parser.error("Cannot return generic scalar",null);
- // With no user constructor, <init> doubles as the public no-arg
- // constructor. Required fields remain TOP in its private memory: a
- // typed parser poison which deliberately emitted no Store or code.
- if( ctrl()._type == Type.CONTROL && _fun.isInstance() ) {
- TypeStruct self = ((TypeMemPtr)_fun.sig().arg(0))._obj;
- if( !CodeGen.hasUserConstructor(self) && expr()._type instanceof TypeMem mem )
- for( Field fld : self._fields ) {
- if( !(fld._t instanceof TypeNil tn && tn.notNull()) ) continue;
- Type actual = mem._alias==1 && mem._t instanceof TypeStruct ts
- ? ts.field(fld._fname)._t
- : mem._alias==fld._alias ? mem._t : Type.TOP;
- if( actual==Type.TOP || actual instanceof TypeNil atn && atn.nullable() )
- return Parser.error("'"+self._name+"' is not fully initialized, field '"+
- fld._fname+"' is only partially set in the constructor",null);
- }
- }
return null;
}
diff --git a/src/main/java/com/seaofnodes/simple/node/SarNode.java b/src/main/java/com/seaofnodes/simple/node/SarNode.java
index 737b531f..2e70f345 100644
--- a/src/main/java/com/seaofnodes/simple/node/SarNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/SarNode.java
@@ -1,11 +1,12 @@
package com.seaofnodes.simple.node;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Lexer;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.type.Type;
import com.seaofnodes.simple.type.TypeInteger;
public class SarNode extends ArithNode {
- public SarNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); }
+ public SarNode(Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); }
@Override public Tag serialTag() { return Tag.Sar; }
@Override public String op() { return ">>"; }
diff --git a/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java
index 694d5dcb..1130cf44 100644
--- a/src/main/java/com/seaofnodes/simple/node/ScopeNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/ScopeNode.java
@@ -1,8 +1,9 @@
package com.seaofnodes.simple.node;
+import com.seaofnodes.simple.parser.Lexer;
import com.seaofnodes.print.ExprPrinter;
-import com.seaofnodes.simple.Parser;
-import com.seaofnodes.simple.Var;
+import com.seaofnodes.simple.parser.Parser;
+import com.seaofnodes.simple.parser.Var;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.codegen.CompUnit;
import com.seaofnodes.simple.codegen.ParseAll;
@@ -25,6 +26,10 @@ public class ScopeNode extends MemMergeNode {
public static final String ARG0 = "arg";
public static final String MEM0 = "$mem";
+ // Structural label for a precise memory alias. The '$' prefix cannot be
+ // referenced in Simple source.
+ public static String memName(int alias) { return ("$"+alias).intern(); }
+
// All active/live variables in all nested scopes, all run together.
// Maps 1-to-1 to the _inputs array.
public final Ary<Var> _vars;
@@ -204,9 +209,8 @@ public class ScopeNode extends MemMergeNode {
return kx == -1 ? null : _kinds.at(kx);
}
- // Final fields are writable local values inside their own initializer or
- // constructor. Final parameters, local variables, and captured bindings
- // remain final. Use the same rule when deciding which loop Phis to build.
+ // Final fields are writable inside their own initializer or constructor.
+ // Final parameters, local variables, and captured bindings remain final.
public boolean canInit(Var v) {
int fx=enclosingFuncOrDecl();
Kind.Func fun=(Kind.Func)_kinds.at(fx);
@@ -232,7 +236,7 @@ public class ScopeNode extends MemMergeNode {
/**
* Create a new variable name in the current scope
*/
- public boolean define( String name, Type declaredType, boolean xfinal, Node init, Parser.Lexer loc ) {
+ public boolean define( String name, Type declaredType, boolean xfinal, Node init, Lexer loc ) {
assert _kinds.isEmpty() || name!=MEM0 ; // Later scopes do not define memory
if( depth() > 0 )
for( int i=_vars.size()-1; i>=klast()._lexSize; i-- ) {
@@ -260,7 +264,7 @@ public class ScopeNode extends MemMergeNode {
// lexical scope. Scope pops promote it outwards "as if" the final constant
// had been declared before the using scope.
- public Var defineFRef( String id, Type t, boolean xfinal, Parser.Lexer loc ) {
+ public Var defineFRef( String id, Type t, boolean xfinal, Lexer loc ) {
Var var = new Var(nIns(),id,t,xfinal,loc,true);
FRefNode fref = new FRefNode(id,loc).init();
fref._type = fref._con = t;
@@ -375,7 +379,7 @@ public class ScopeNode extends MemMergeNode {
* @param that The ScopeNode to be merged into this
* @return A new node representing the merge point
*/
- public RegionNode mergeScopes(ScopeNode that, Parser.Lexer loc) {
+ public RegionNode mergeScopes(ScopeNode that, Lexer loc) {
RegionNode r = ctrl(new RegionNode(loc,null,ctrl(), that.ctrl()).keep());
this._merge(that,r);
that.kill(); // Kill merged scope
@@ -423,7 +427,6 @@ public class ScopeNode extends MemMergeNode {
Var n = scope.var(i);
if( n.isFRef() ) { // Loop body has forward refs
Var v = new Var(nIns(),n._name,n.type(),n._final,n._loc,true);
- v._uninit = n._uninit;
_vars.add(v);
addDef(scope.in(i));
}
diff --git a/src/main/java/com/seaofnodes/simple/node/ShlNode.java b/src/main/java/com/seaofnodes/simple/node/ShlNode.java
index f657cf51..fffc71f0 100644
--- a/src/main/java/com/seaofnodes/simple/node/ShlNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/ShlNode.java
@@ -1,12 +1,13 @@
package com.seaofnodes.simple.node;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Lexer;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.type.Type;
import com.seaofnodes.simple.type.TypeInteger;
public class ShlNode extends ArithNode {
- public ShlNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); }
+ public ShlNode(Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); }
@Override public Tag serialTag() { return Tag.Shl; }
@Override public String op() { return "<<"; }
diff --git a/src/main/java/com/seaofnodes/simple/node/ShrNode.java b/src/main/java/com/seaofnodes/simple/node/ShrNode.java
index e93ea0e8..1bf86f90 100644
--- a/src/main/java/com/seaofnodes/simple/node/ShrNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/ShrNode.java
@@ -1,11 +1,12 @@
package com.seaofnodes.simple.node;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Lexer;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.type.Type;
import com.seaofnodes.simple.type.TypeInteger;
public class ShrNode extends ArithNode {
- public ShrNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); }
+ public ShrNode(Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); }
@Override public Tag serialTag() { return Tag.Shr; }
@Override public String op() { return ">>>"; }
diff --git a/src/main/java/com/seaofnodes/simple/node/StartNode.java b/src/main/java/com/seaofnodes/simple/node/StartNode.java
index 0acc1d48..36583884 100644
--- a/src/main/java/com/seaofnodes/simple/node/StartNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/StartNode.java
@@ -3,7 +3,7 @@ package com.seaofnodes.simple.node;
import com.seaofnodes.print.ExprPrinter;
import com.seaofnodes.simple.codegen.CodeGen;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.Utils;
import java.util.Arrays;
diff --git a/src/main/java/com/seaofnodes/simple/node/StoreNode.java b/src/main/java/com/seaofnodes/simple/node/StoreNode.java
index 2c62db7d..010d5f61 100644
--- a/src/main/java/com/seaofnodes/simple/node/StoreNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/StoreNode.java
@@ -1,5 +1,7 @@
package com.seaofnodes.simple.node;
+import com.seaofnodes.simple.parser.Parser;
+import com.seaofnodes.simple.parser.Lexer;
import com.seaofnodes.print.ExprPrinter;
import com.seaofnodes.simple.*;
@@ -29,10 +31,10 @@ public class StoreNode extends MemOpNode {
* @param off The offset inside the struct base
* @param value Value to be stored
*/
- public StoreNode(Parser.Lexer loc, String name, int alias, Type decl, Node ctrl, Node mem, Node ptr, Node off, Node value, boolean init) {
+ public StoreNode(Lexer loc, String name, int alias, Type decl, Node ctrl, Node mem, Node ptr, Node off, Node value, boolean init) {
this(loc,name,alias,decl,ctrl,mem,ptr,off,value,init,(byte)0);
}
- private StoreNode(Parser.Lexer loc, String name, int alias, Type decl, Node ctrl, Node mem, Node ptr, Node off, Node value, boolean init, byte size) {
+ private StoreNode(Lexer loc, String name, int alias, Type decl, Node ctrl, Node mem, Node ptr, Node off, Node value, boolean init, byte size) {
super(loc, name, alias, false, decl, ctrl, mem, ptr, off, value);
_init = init;
_size = size;
@@ -78,7 +80,7 @@ public class StoreNode extends MemOpNode {
Type mem0 = mem()._type;
Type ptr0 = ptr()._type;
// Validate argument types
- if( ptr0.isHigh() )
+ if( ptr0.isHigh() || mem0.isHigh() )
return TypeMem.TOP;
if( !(mem0 instanceof TypeMem mem) ||
!(ptr0 instanceof TypeMemPtr ptr) )
@@ -86,9 +88,16 @@ public class StoreNode extends MemOpNode {
// Sharpen memory value; required for narrowing stores where the parser
// inserts zero/sign masking and somebody reads the TypeMem type.
+ // A declared bound must not turn an unwritten value into an
+ // initialized integer, float, or pointer.
Type decl = declaredType();
- if( decl != Type.BOTTOM )
+ if( decl instanceof TypeMemPtr ptrType ) decl = ptrType.withPartial(TypeMemPtr.mayBePartial(val));
+ if( decl != Type.BOTTOM && val!=Type.BOTTOM && val!=TypeScalar.BOT )
{ assert decl != Type.TOP; val = val.join(decl); }
+ // A constructor can itself be a field. Its result memory contains
+ // that field again; retain the declared signature instead of unfolding
+ // successively sharper copies of its private-memory return type.
+ if( mem._one && decl instanceof TypeFunPtr && val instanceof TypeFunPtr ) val = decl;
TypeMem outside = TypeMem.opaque(val) ? CodeGen.CODE._start.outside(this) : TypeMem.BOT;
// A singleton class's final constant field lives in the static image.
// Its precise memory slice is a constant too; ordinary peepholes can
@@ -96,10 +105,12 @@ public class StoreNode extends MemOpNode {
// A constant runtime value alone is not enough: mutable fields (notably
// $init) still need their writes, and instance fields need allocation.
Field fld = ptr._obj.field(_name);
- if( _alias!=1 && ptr._one && Parser.startsClzPrefix(ptr._obj._name) &&
+ if( fld==null && off() instanceof ConFldOffNode off ) fld = off._ts.field(_name);
+ if( _alias!=1 && ptr._one && TypeStruct.startsClzPrefix(ptr._obj._name) &&
fld!=null && fld._final && decl.isConstant() )
- // A constant slice contains only this value's escapes. Copying
- // bulk escapes here would freeze a transient FULL into it.
+ // This singleton slice contains just the stored value. Copying
+ // bulk memory's escapes would freeze a transient FULL bound into
+ // the constant, permanently exposing unrelated private functions.
return TypeMem.make(_alias,val,true,true,true,null,null).escapesFrom(val,outside);
// Allocation uses a private TypeMem and nothing else does. This
@@ -107,7 +118,7 @@ public class StoreNode extends MemOpNode {
// which is never does in a constructor.
if( !_name.equals("[]") && mem._one )
// Just track the stored value
- return TypeMem.make(_alias,val,true,false,_init,null,null).escapesFrom(val,outside);
+ return TypeMem.make(_alias,val,true,false,fld==null ? _init : fld._final,null,null).escapesFrom(val,outside);
// Normal aliasing Store.
assert mem._alias==1 || mem._alias==_alias; // Perfect aliasing
@@ -320,6 +331,11 @@ public class StoreNode extends MemOpNode {
public Parser.ParseException err() {
Parser.ParseException err = super.err();
if( err != null ) return err;
+ if( !_init && ptr()._type instanceof TypeMemPtr p && p._partial )
+ return Parser.error("Cannot modify a field through an incomplete object outside its constructor",_loc);
+ if( TypeMemPtr.mayBePartial(val()._type) &&
+ !(mem()._type instanceof TypeMem mem && mem._one && !mem._clz) )
+ return Parser.error("Cannot store an incomplete object into an ordinary object",_loc);
assert _size>0 || CodeGen.CODE._phase.ordinal() <= CodeGen.Phase.FinalizeIR.ordinal();
if( ptr()._type == Type.TOP ) // Dead store?
return null;
diff --git a/src/main/java/com/seaofnodes/simple/node/SubNode.java b/src/main/java/com/seaofnodes/simple/node/SubNode.java
index 8eb1f4cc..c2e285c9 100644
--- a/src/main/java/com/seaofnodes/simple/node/SubNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/SubNode.java
@@ -1,6 +1,6 @@
package com.seaofnodes.simple.node;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.type.Type;
import com.seaofnodes.simple.type.TypeInteger;
import java.util.BitSet;
diff --git a/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java b/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java
index 151a326e..472fff7b 100644
--- a/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/ToFloatNode.java
@@ -4,6 +4,7 @@ package com.seaofnodes.simple.node;
import com.seaofnodes.simple.type.Type;
import com.seaofnodes.simple.type.TypeFloat;
import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.type.TypeScalar;
public class ToFloatNode extends Node {
@@ -15,6 +16,7 @@ public class ToFloatNode extends Node {
@Override
public Type compute() {
if( in(1)._type.isHigh() ) return TypeFloat.F64.dual();
+ if( in(1)._type==Type.BOTTOM || in(1)._type==TypeScalar.BOT ) return in(1)._type;
if( in(1)._type == Type.NIL ) return TypeFloat.FZERO;
if( in(1)._type instanceof TypeInteger i0 && i0.isConstant() )
return TypeFloat.constant(i0.value());
diff --git a/src/main/java/com/seaofnodes/simple/node/ToIntegerNode.java b/src/main/java/com/seaofnodes/simple/node/ToIntegerNode.java
index 33806300..2ae45740 100644
--- a/src/main/java/com/seaofnodes/simple/node/ToIntegerNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/ToIntegerNode.java
@@ -3,6 +3,7 @@ package com.seaofnodes.simple.node;
import com.seaofnodes.simple.type.Type;
import com.seaofnodes.simple.type.TypeFloat;
import com.seaofnodes.simple.type.TypeInteger;
+import com.seaofnodes.simple.type.TypeScalar;
// Truncate toward zero, saturating overflow; NaN converts to zero.
public class ToIntegerNode extends Node {
@@ -14,6 +15,7 @@ public class ToIntegerNode extends Node {
@Override public Type compute() {
Type t = in(1)._type;
if( t.isHigh() ) return TypeInteger.TOP;
+ if( t==Type.BOTTOM || t==TypeScalar.BOT ) return t;
if( t instanceof TypeFloat tf && tf.isConstant() )
return TypeInteger.constant((long)tf.value());
return TypeInteger.BOT;
diff --git a/src/main/java/com/seaofnodes/simple/node/TypeNode.java b/src/main/java/com/seaofnodes/simple/node/TypeNode.java
index 3ab2d6f9..38b0b288 100644
--- a/src/main/java/com/seaofnodes/simple/node/TypeNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/TypeNode.java
@@ -1,6 +1,6 @@
package com.seaofnodes.simple.node;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.BAOS;
import com.seaofnodes.simple.util.Utils;
diff --git a/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java b/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java
index 32e1deb9..d727ce2b 100644
--- a/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/XCtrlNode.java
@@ -3,7 +3,7 @@ package com.seaofnodes.simple.node;
import com.seaofnodes.print.ExprPrinter;
import com.seaofnodes.simple.codegen.CodeGen;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.type.Type;
public class XCtrlNode extends CFGNode {
diff --git a/src/main/java/com/seaofnodes/simple/node/XorNode.java b/src/main/java/com/seaofnodes/simple/node/XorNode.java
index c825f396..85fb3259 100644
--- a/src/main/java/com/seaofnodes/simple/node/XorNode.java
+++ b/src/main/java/com/seaofnodes/simple/node/XorNode.java
@@ -1,11 +1,12 @@
package com.seaofnodes.simple.node;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Lexer;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.type.Type;
import com.seaofnodes.simple.type.TypeInteger;
public class XorNode extends ArithNode {
- public XorNode(Parser.Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); }
+ public XorNode(Lexer loc, Node lhs, Node rhs) { super(loc, lhs, rhs, (byte)1); }
@Override public Tag serialTag() { return Tag.Xor; }
@Override public String op() { return "^"; }
diff --git a/src/main/java/com/seaofnodes/simple/parser/Allocations.java b/src/main/java/com/seaofnodes/simple/parser/Allocations.java
new file mode 100644
index 00000000..be68e10e
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/Allocations.java
@@ -0,0 +1,214 @@
+package com.seaofnodes.simple.parser;
+
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+
+/** Allocation expressions and declaration-side allocation helper graphs. */
+final class Allocations {
+ private final Parser P;
+
+ Allocations(Parser P) { this.P = P; }
+
+ /**
+ Parse an allocation
+ */
+ Node parseNew() {
+ Type t = P.types.parseType();
+ if( t==null ) {
+ String tname = P._lexer.matchId();
+ if( tname==null || Parser.KEYWORDS.contains(tname) )
+ throw P.error("Expected a type");
+ t = P.types.forwardRefType(tname);
+ }
+ // Parse ary[ length_expr ]
+ if( P.match("[") ) {
+ if( !t.makeZero().isa(t) )
+ throw P.error("Cannot allocate a non-nullable, since arrays are always zero/null filled");
+ Node len = P.expressions.parseAssignment().keep();
+ P.require("]");
+ return allocArray(P.types.typeAry(t,false),len);
+ } else {
+ if( !(t instanceof TypeMemPtr tmp) )
+ throw P.error("Cannot allocate a "+t.str());
+ return allocStruct(tmp._obj);
+ }
+ }
+
+ private Node allocStruct(TypeStruct ts) {
+ Ary<Node> ctorArgs = constructorArgs();
+ // A field declaration can leave a short forward type name in TYPES.
+ // Allocation happens in lexical context, so resolve that short name
+ // to the completed nested type before selecting its class helper.
+ String typeName = ts._name.indexOf('.') == -1 ? P.types.fullTypeName(ts._name) : ts._name;
+ Type resolved = Parser.TYPES.get(typeName);
+ if( resolved instanceof TypeStruct rts ) ts = rts;
+ TypeStruct clz = (TypeStruct)Parser.TYPES.get(TypeStruct.addClzPrefix(typeName));
+ if( clz==null )
+ throw P.error("Unknown struct type '" + ts._name + "'");
+ Field afld = clz.field("<new>");
+ Node alloc;
+ if( afld != null ) {
+ TypeFunPtr tfp = (TypeFunPtr)afld._t;
+ FunNode fun = P._code.link(tfp);
+ alloc = fun==null
+ ? new FRefNode((TypeStruct.addClzPrefix(typeName)+".<new>").intern(),P.loc()).init()
+ : new FunPtrNode(tfp,P._code._start,fun.ret()).peephole();
+ } else {
+ if( !ts._fref )
+ throw P.error("Constructor arguments for '" + ts._name + "' but no constructor is defined");
+ Type[] sig = new Type[ctorArgs._len];
+ for( int i = 0; i<ctorArgs._len; i++ )
+ sig[i] = ctorArgs.at(i)._type;
+ alloc = new FRefNode((TypeStruct.addClzPrefix(typeName)+".<new>").intern(),P.loc(),
+ TypeFunPtr.make(false,sig,TypeMemPtr.make(ts))).init();
+ }
+ Ary<Node> args = new Ary<>(Node.class);
+ args.add(P.ctrl()); args.add(P.mem());
+ for( Node arg : ctorArgs ) args.add(arg.unkeep());
+ args.add(alloc);
+ Node result = P.functions.functionCall(args);
+ if( P.ctrl()._type==Type.XCONTROL ) return result;
+ return new ConstructNode(result,ctorArgs).peephole();
+ }
+
+ private Ary<Node> constructorArgs() {
+ Ary<Node> args = new Ary<>(Node.class);
+ if( !P.match("(") )
+ return args;
+ P.functions.arguments(args);
+ P.require(")");
+ return args;
+ }
+
+ private Node allocArray(TypeStruct ts, Node len) {
+ Field lenFld = ts.field("#" );
+ Field bodyFld = ts.field("[]");
+
+ Node base = ConFldOffNode.make(ts, "[]");
+ int scale = ts.aryScale();
+ Node size = P.peep(new AddNode(base,P.peep(new ShlNode(null,len,P.con(scale)))));
+
+ // Build a NewNode; takes in and puts out all aliases.
+ NewNode nnn = new NewNode(ts, P.ctrl(), size ).init();
+ ProjNode self = new ProjNode(nnn,0,ts.str()).init().keep();
+ ProjNode smem = new ProjNode(nnn,1,"#selfMem").init().keep();
+
+ // Store length. Rest of array is zero'd via CALLOC during CodeGen.
+ // Length is casted to sanity.
+ // TODO: Needs runtime check.
+ Node st = P.peep(new StoreNode(null, "#", 1, TypeInteger.U32, null, smem, self, ConFldOffNode.make(ts,"#"), len.unkeep(), true));
+
+ // TODO: Allow add-on "constructor" to init the array elements
+
+ Node prior = P.mem();
+ Node esc = P.peep(new EscapeNode(lenFld,self,st,prior));
+ P.mem(P.mergeAlias(prior,lenFld._alias,esc));
+ prior = P.mem();
+ esc = P.peep(new EscapeNode(bodyFld,self,smem,prior));
+ P.mem(P.mergeAlias(prior,bodyFld._alias,esc));
+ smem.unkeep();
+ return self.unkeep();
+ }
+
+ Node newString(String s) {
+ TypeStruct ts = P.types.typeAry(TypeInteger.U8,true);
+ int lenAlias = ts.field("#" )._alias;
+ int elemAlias = ts.field("[]")._alias;
+ TypeConAryB body = TypeConAryB.make(s);
+ TypeInteger slen = TypeInteger.constant(s.length());
+ // Make a TMP, not-null (byte)2, singleton (true) (requires text
+ // strings are hash-interned), with a TypeStruct having a constant
+ // array body.
+ TypeMemPtr str = TypeMemPtr.make((byte)2,TypeStruct.makeAry("[]u8", slen, lenAlias, body, elemAlias, true),true);
+ assert str.isConstant();
+ return P.con(str.makeRO());
+ }
+
+ // Build the declaration-side allocation helper. Allocation sites only
+ // call this function; all layout, initialization and publication choices
+ // are made here, where the complete struct and constructor are known.
+ TypeFunPtr makeAllocator(TypeStruct ts, ReturnNode initRet, Field ctorFld) {
+ if( ctorFld==null ) {
+ Field legacy = ts.field(ts._name.substring(ts._name.lastIndexOf('.')+1).intern());
+ if( legacy!=null && legacy._t instanceof TypeFunPtr sig && sig.nargs()>=2 && sig.arg(1) instanceof TypeMem )
+ ctorFld = legacy;
+ }
+ TypeFunPtr ctor = ctorFld==null ? null : (TypeFunPtr)ctorFld._t;
+ int nargs = ctor==null ? 0 : ctor.nargs()-2; // Hide self and selfMem
+ Type[] atypes = new Type[nargs];
+ for( int i = 0; i<nargs; i++ ) atypes[i] = ctor.arg(i+2);
+ TypeMemPtr ptr = TypeMemPtr.make(ts);
+ TypeFunPtr sig = TypeFunPtr.make1((byte)2,true,atypes,ptr,P._code.fidx(P._ref._cname));
+ String name = (ts._name+".<new>").intern();
+
+ int oldUID = P._code.UID();
+ FunNode fun = (FunNode)new FunNode(P.loc(),sig,name,P._ref,null,P._ref._start).peephole();
+ P._code.link(fun);
+ Node rpc = new ParmNode("$rpc",0,TypeRPC.BOT,fun,ConstantNode.seed(TypeRPC.BOT).peephole()).peephole();
+ Node pub = new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun,
+ new ProjNode(P._ref._start,1,ScopeNode.MEM0).peephole()).peephole();
+ Node[] parms = new Node[nargs];
+ for( int i = 0; i<nargs; i++ )
+ parms[i] = new ParmNode("arg"+i,i+2,atypes[i],fun,ConstantNode.seed(fun.defaultArg(i)).peephole()).peephole();
+
+ NewNode nnn = new NewNode(ts,fun,ConFldOffNode.make(ts," len")).init();
+ Node self = new ProjNode(nnn,0,ts.str()).init();
+ Node priv = new ProjNode(nnn,1,"#selfMem").init();
+ Node[] initArgs = new Node[]{fun,pub,self,priv,
+ new FunPtrNode(initRet.fun().sig(),P._code._start,initRet).peephole()};
+ CallEndNode cend = rawCall(initArgs,initRet.fun());
+ Node ctl = new CProjNode(cend,0,ScopeNode.CTRL).peephole();
+ pub = new ProjNode(cend,1,ScopeNode.MEM0).peephole();
+ priv = new ProjNode(cend,2,"#selfMem").peephole();
+
+ if( ctor != null ) {
+ Node[] cargs = new Node[nargs+5];
+ cargs[0] = ctl; cargs[1] = pub; cargs[2] = self; cargs[3] = priv;
+ for( int i = 0; i<nargs; i++ ) cargs[i+4] = parms[i];
+ FunNode cfun = P._code.link(ctor);
+ cargs[cargs.length-1] = new FunPtrNode(ctor,P._code._start,cfun.ret()).peephole();
+ cend = rawCall(cargs,cfun);
+ ctl = new CProjNode(cend,0,ScopeNode.CTRL).peephole();
+ pub = new ProjNode(cend,1,ScopeNode.MEM0).peephole();
+ priv = new ProjNode(cend,2,"#selfMem").peephole();
+ }
+
+ // Hidden construction helpers have only allocator callers.
+ if( initRet.fun().unknownCallers() ) initRet.fun().removeDeadPath(1);
+ if( ctor!=null ) {
+ FunNode cfun = P._code.link(ctor);
+ if( cfun.unknownCallers() ) cfun.removeDeadPath(1);
+ }
+
+ priv.keep();
+ for( Field field : ts._fields ) {
+ Type storage = field._t.makeStorage();
+ Field escaped = field._final ? field : field.makeFrom(storage).makeFrom(true);
+ pub.keep();
+ Node esc = new EscapeNode(escaped,self,priv,pub).peephole();
+ MemMergeNode merge = new MemMergeNode(null,pub);
+ merge.alias(field._alias,esc);
+ Node next = merge.peephole();
+ pub.unkeep();
+ pub = next;
+ }
+ Node finished = new ConstructNode(self,priv,P.loc(),ctor!=null).peephole();
+ priv.unkeep();
+ ReturnNode ret = (ReturnNode)new ReturnNode(ctl,pub,finished,rpc,fun).peephole();
+ fun.setRet(ret);
+ fun._approxUIDs = P._code.UID() - oldUID;
+ P._ref.addFun(P._code,fun);
+ return sig;
+ }
+
+ private CallEndNode rawCall(Node[] args, FunNode target) {
+ // Establish this known declaration-side edge immediately, but do not
+ // peephole or inline it while the surrounding factory is being built.
+ // IterPeeps therefore starts with the complete initializer caller set.
+ CallNode call = new CallNode(P.loc(),args).init();
+ CallEndNode cend = new CallEndNode(call,TypeRPC.constant(P._code.rpc(P._ref._cname))).init();
+ call.link(target);
+ return cend;
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Construction.java b/src/main/java/com/seaofnodes/simple/parser/Construction.java
new file mode 100644
index 00000000..6dec8ac1
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/Construction.java
@@ -0,0 +1,130 @@
+package com.seaofnodes.simple.parser;
+
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+
+/**
+ * Field initialization is ordinary forward typing: New supplies Scalar.BOT
+ * fields in private memory, Store replaces a field, and Phi meets the incoming
+ * memories. Scalar.BOT survives a missing live arm; Integer.BOT describes an
+ * initialized integer. Loads and merged Returns inspect these types after the
+ * parser closes loops and compute() stabilizes. No backwards initialization walk.
+ *
+ * Constructor self and ^ parameters are incomplete. InitViewNode combines self
+ * with its current private memory: set fields carry their declared types and
+ * missing fields carry Scalar.BOT. Local ^ parameters meet these shapes through
+ * ordinary forward flow; unknown callers promise no initialized fields. Only
+ * the original constructor may write through an incomplete pointer.
+ *
+ * Calls borrowing self receive its Stores on the ordinary public memory edge,
+ * using aliasable Escape slices without publishing its identity. Field values
+ * come from memory, not the pointer's initialization shape. Incomplete values
+ * may be retained only in private construction memory. Nested object references
+ * conservatively promise no initialized fields; arrays are born initialized.
+ * No field requirements are inferred and no extra memory arguments are added.
+ *
+ * Incompleteness lives in TypeMemPtr, not a parser provenance graph. Ordinary
+ * Phis carry it around loops, conversions preserve it, and ConstructNode types
+ * each allocation result from its actual arguments. Errors belong to the IR
+ * nodes and are checked at TypeCheck; the parser keeps no source-use lists.
+ *
+ * The qualifier is a two-point lattice: complete meets incomplete to incomplete.
+ * Constructor self and ^ formals seed incomplete; a ^ formal stays restricted
+ * even if its known callers are complete. A ConstructNode wraps each source new
+ * and ORs the actual argument qualifiers. Thus new Child(self) is incomplete,
+ * while new Child(null) and new Child(oldParent) are complete, even though all
+ * three calls share the same constructor Return. The raw allocator result is
+ * complete after its own fields have been checked; ConstructNode adds the
+ * caller's restriction and disappears after TypeCheck.
+ *
+ * Reading a pointer field through a completed receiver gives a completed view
+ * of that field too: its object's construction could only retain incomplete
+ * references from the enclosing construction. There is no group ID to clear.
+ * A read through an incomplete pointer conservatively returns an incomplete reference
+ * (except initialized arrays), even when the stored value happens to be complete.
+ * Own-self access is identified by the receiver and its private memory, not by
+ * the nominal class: a ^ argument of the same class cannot use that exception.
+ *
+ * The existing SCCP pass solves qualifier cycles optimistically. For p = null followed
+ * by p = new Link(p) in a loop, nothing introduces incompleteness, so the loop
+ * stays complete. A backedge carrying self makes the Phi and its uses incomplete.
+ * Both cases use the ordinary worklist; no recursive Load/Phi/Call query remains.
+ * A partial-only ConstructNode supplies the body view of self and ^ formals.
+ * The Parm itself merges callers normally; inlining cannot erase the view.
+ * Hidden helpers have only allocator callers, while the public constructor
+ * binding denotes the allocator. Private-memory Parms therefore need no
+ * fictitious external default or constructor-specific transfer function.
+ * InitView and Construct are erased by the existing TypeCheck walk. There is
+ * no extra construction pass; see docs/construction-groups.md.
+ */
+final class Construction {
+ private final Parser P;
+ boolean _borrowed; // Source ^ parameters; separate compilation is still deferred.
+ Frame _frame;
+
+ Construction(Parser P) { this.P = P; }
+
+ static final class Frame {
+ final boolean _owner;
+ final Node _self;
+ Frame(boolean owner, Node self) { _owner = owner; _self = self; }
+ }
+
+ Frame enter(FunNode fun, boolean owner) {
+ Frame old = _frame;
+ _frame = new Frame(owner,fun.parm(2));
+ return old;
+ }
+
+ Node memory(Node ptr) {
+ return own(ptr) ? selfMem() : P.mem();
+ }
+
+ Node selfMem() {
+ Var mem = P._scope.lookup("#selfMem");
+ return P._scope.in(P._scope.update(mem,null));
+ }
+
+ Node view(Node ptr) {
+ return own(ptr) ? new InitViewNode((TypeMemPtr)((ParmNode)_frame._self)._declaredType,ptr,selfMem()).peephole() : ptr;
+ }
+
+ // Carry the constructor's Stores on the call's ordinary memory edge.
+ // The borrowed receiver is read-only; this does not publish its identity.
+ Node callMemory(Ary<Node> args, Node pub) {
+ if( !_frame._owner ) return pub;
+ boolean lends = false;
+ for( int i=2; i<args.size()-1; i++ ) lends |= own(args.at(i));
+ if( !lends ) return pub;
+ Node self = _frame._self;
+ TypeStruct ts = (TypeStruct)Parser.TYPES.get(((TypeMemPtr)self._type)._obj._name);
+ Node priv = selfMem().keep();
+ for( Field fld : ts._fields ) {
+ pub.keep();
+ Node loan = new EscapeNode(fld,self,priv,pub,true).peephole();
+ Node next = P.mergeAlias(pub,fld._alias,loan);
+ pub.unkeep();
+ pub = next;
+ }
+ priv.unkeep();
+ return pub;
+ }
+
+ boolean own(Node ptr) {
+ while( ptr instanceof InitViewNode || ptr instanceof ConvertNode || ptr instanceof ReadOnlyNode || ptr instanceof GuardNode || (ptr instanceof ConstructNode con && con._partial) ) ptr = ptr.in(1);
+ return _frame._owner && ptr==_frame._self;
+ }
+
+ Node storeMemory(Node ptr) { return own(ptr) ? selfMem() : P.mem(); }
+
+ void store(Node ptr, int alias, Node st, Node prior) {
+ if( own(ptr) ) P._scope.update("#selfMem",P.mergeAlias(prior,alias,st));
+ else P.storeMem(st,alias,prior);
+ }
+
+ void checkSerializable() {
+ if( _borrowed )
+ throw P.error("Separate compilation of functions with '^' parameters is not supported yet");
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/parser/ControlFlow.java b/src/main/java/com/seaofnodes/simple/parser/ControlFlow.java
new file mode 100644
index 00000000..caa73076
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/ControlFlow.java
@@ -0,0 +1,96 @@
+package com.seaofnodes.simple.parser;
+
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.parser.Parser.ParseException;
+import com.seaofnodes.simple.type.*;
+
+import static com.seaofnodes.simple.util.Utils.TODO;
+
+/** Shared guarded branch/merge construction for if, ?:, && and ||. */
+final class ControlFlow {
+ private final Parser P;
+
+ ControlFlow(Parser P) { this.P = P; }
+
+ // Parse a conditional expression, merging results.
+ Node conditional( Node pred, String fside ) {
+ // IfNode takes current control and predicate
+ pred.keep();
+ Node ifNode = new IfNode(P.ctrl(), pred).peephole();
+ // Setup projection nodes
+ CFGNode ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep();
+ CFGNode ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole().keep();
+ // In if true branch, the ifT proj node becomes the ctrl
+ // But first clone the scope and set it as current
+ ScopeNode fScope = P._scope.dup(); // Duplicate current scope
+ fScope.ctrl(ifF); // The pending arm already starts at False
+ P._xScopes.push(fScope); // For graph visualization we need all scopes
+
+ // Parse the true side
+ P.ctrl(ifT.unkeep()); // set ctrl token to ifTrue projection
+ P._scope.addGuards(ifT,pred,false); // Up-cast predicate
+ // Parse true-side flavor
+ Node lhs = switch( fside ) {
+ case "else" -> P.statements.parseStatement(); // if( pred ) stmts;
+ case ":" -> P.expressions.parseAssignment(); // pred ? asgn;
+ case "&&" -> P.expressions.parseLogical(); // pred && expr
+ case "||" -> pred; // pred || expr_is_ignored
+ default -> throw TODO();
+ };
+ lhs.keep();
+ P._scope.removeGuards(ifT);
+
+ // See if a one-sided def was made: "if(pred) int x = 1;" and throw.
+ // See if any forward-refs were made, and copy them to the other side:
+ // "pred ? n*fact(n-1) : 1"
+ fScope.balanceIf(P._scope);
+
+ ScopeNode tScope = P._scope;
+
+ // Parse the false side
+ P._scope = fScope; // Restore scope, then parse else block if any
+ ifF.unkeep(); // fScope already owns the false control
+ // Up-cast predicate, even if not else clause, because predicate can
+ // remain true if the true clause exits: `if( !ptr ) return 0; return ptr.fld;`
+ P._scope.addGuards(ifF,pred,true);
+ // Parse false-side flavor
+ boolean doRHS = false; // RHS is optional for if/else and trinary
+ Node rhs = switch( fside ) {
+ case "else" -> (doRHS = P.match(fside)) ? P.statements.parseStatement() : P.con(lhs._type.makeZero());
+ case ":" -> (doRHS = P.match(fside)) ? P.expressions.parseAssignment() : P.con(lhs._type.makeZero());
+ case "&&" -> P.con(lhs._type.makeZero()); // pred && expr_is_ignored
+ case "||" -> P.expressions.parseLogical(); // pred || expr
+ default -> throw TODO();
+ };
+ rhs.keep();
+ P._scope.removeGuards(ifF);
+ // The RHS of `||` is parsed on the false side, so retain its updated
+ // control, memory and locals just as an explicit else arm does.
+ if( doRHS || fside.equals("||") )
+ fScope = P._scope;
+ pred.unkeep();
+
+ // Check the trinary widening int/flt
+ if( !fside.equals("else") ) {
+ rhs = P.conversions.widenInt( rhs.unkeep(), lhs._type ).keep();
+ lhs = P.conversions.widenInt( lhs.unkeep(), rhs._type ).keep();
+ }
+
+ P._scope = tScope;
+ P._xScopes.pop(); // Discard pushed from graph display
+ // See if a one-sided def was made: "if(pred) int x = 1;" and throw.
+ // See if any forward-refs were made, and copy them to the other side:
+ // "pred ? n*fact(n-1) : 1"
+ tScope.balanceIf(fScope);
+
+ // Merge results
+ RegionNode r = P.ctrl(tScope.mergeScopes(fScope,P.loc()));
+ Node ret = P.peep(new PhiNode("",r,lhs.unkeep(),rhs.unkeep()));
+ // Immediately fail e.g. `arg ? 7 : ptr`
+ ParseException err;
+ if( !fside.equals("else") && (err = ret.err()) !=null ) throw err;
+ ret.keep();
+ r.peephole();
+ return ret.unkeep();
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Conversions.java b/src/main/java/com/seaofnodes/simple/parser/Conversions.java
new file mode 100644
index 00000000..fa11311d
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/Conversions.java
@@ -0,0 +1,72 @@
+package com.seaofnodes.simple.parser;
+
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.type.*;
+
+/** Shared declaration, assignment, and return conversions. */
+final class Conversions {
+ private final Parser P;
+
+ Conversions(Parser P) { this.P = P; }
+
+ // Widen ints to floats; narrow wide int types.
+ // Early error if types do not match variable.
+ Node convertExpr( Node expr, Type t ) {
+ if( !expr._type.accessISA(t) )
+ throw P.error("Type "+expr._type.str()+" is not of declared type "+t.str());
+ if( t instanceof TypeMemPtr tmp && tmp.isFinal() )
+ expr = P.peep(new ReadOnlyNode(expr));
+ // A FunPtr carries the semantic edge to its function's Return. Do not
+ // hide that edge behind a Convert which can fold to a plain constant.
+ if( expr instanceof FunPtrNode && t instanceof TypeFunPtr )
+ return expr;
+ // Auto-widen array to i64 (cast ptr to raw int bits)
+ if( t == TypeInteger.BOT && expr._type instanceof TypeMemPtr tmp && tmp._obj.isAry() )
+ return P.peep(new AddNode(P.peep(new PtrToIntNode(expr)),ConFldOffNode.make(tmp._obj,"[]")));
+ // Widen ints to floats, narrow wide types
+ return P.peep(new ConvertNode(t,expr));
+ }
+
+ // Inferred `var` declarations describe writable slots, not the exact
+ // value used to initialize them. Explicitly typed locals retain the
+ // user's nullability and numeric-width guarantees; `val` retains the
+ // initializer's precise type.
+ static Type mutableInferredType(Type t) {
+ return switch( t ) {
+ case TypeInteger ti -> TypeInteger.BOT;
+ case TypeFloat tf -> TypeFloat.F64;
+ case TypeMemPtr tmp -> tmp.makeVar();
+ case TypeFunPtr tfp -> tfp;
+ case TypeConAry<?> a -> a;
+ case TypePtr ptr -> TypePtr.PTR;
+ case TypeScalar ts -> TypeScalar.BOT;
+ default -> t;
+ };
+ }
+
+ Type constructorFieldType(Type t) {
+ if( t instanceof TypeMemPtr ptr && ptr._obj.isConAry() ) {
+ TypeStruct declared = (TypeStruct)Parser.TYPES.get(ptr._obj._name);
+ TypeStruct shape = P.types.typeAry(declared._fields[1]._t,ptr._obj._fields[1]._final);
+ return TypeMemPtr.make((byte)2,shape,false).withAccess(ptr._ro);
+ }
+ return mutableInferredType(t);
+ }
+
+ // Seed an inferred return merge with the declared numeric family instead
+ // of a transient constant/range. Pointer nullability remains part of the
+ // inferred function result and is combined by the later Phi.
+ static Type inferredReturnType(Type t) {
+ return switch( t ) {
+ case TypeInteger ti -> TypeInteger.BOT;
+ case TypeFloat tf -> TypeFloat.F64;
+ default -> t;
+ };
+ }
+
+ Node widenInt( Node expr, Type t ) {
+ return (expr._type instanceof TypeInteger || expr._type==Type.NIL) && t instanceof TypeFloat
+ ? P.peep(new ToFloatNode(expr)) : expr;
+ }
+
+}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Declarations.java b/src/main/java/com/seaofnodes/simple/parser/Declarations.java
new file mode 100644
index 00000000..91955197
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/Declarations.java
@@ -0,0 +1,164 @@
+package com.seaofnodes.simple.parser;
+
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.node.ScopeNode.Kind;
+import com.seaofnodes.simple.type.*;
+
+/** Local/field bindings and native symbol declarations. */
+final class Declarations {
+ private final Parser P;
+
+ Declarations(Parser P) { this.P = P; }
+
+ /**
+ * Parse declaration or expression statement
+ * declStmt = type var['=' exprAsgn][, var['=' exprAsgn]]* ';' | exprAsgn ';'
+ * <p>
+ * exprAsgn = var '=' exprAsgn | expr
+ */
+ Node parseDeclarationStatement() {
+ int old = P.pos();
+ Type t = P.types.parseType();
+ if( P.peek('.') ) // Ambiguity static vars: "type.var", parse as expression
+ { P.pos(old); t = null; }
+ if( t == null )
+ return P.require(P.expressions.parseAssignment(),";");
+
+ // now parse var['=' asgnexpr] in a loop
+ Node n = parseDeclaration(t);
+ while( P.match(",") )
+ n = parseDeclaration(t);
+ return P.require(n,";");
+ }
+
+ // The binding modifier sits immediately before the name. It never
+ // changes permissions on the referenced object.
+ boolean bindingFinal(Type t) {
+ if( P.match("!") ) return false;
+ if( P.match("~") ) return true;
+ return t==Type.TOP || t instanceof TypeNil;
+ }
+
+ /** Parse final: [!|~]var['=' asgn]
+ */
+ private Node parseDeclaration(Type t) {
+ assert t!=null;
+ // Has var/val instead of a user-declared type
+ boolean inferType = t==Type.TOP || t==Type.BOTTOM;
+ boolean xfinal = bindingFinal(t);
+ Lexer loc = P.loc();
+ String name = P.requireId();
+ // Optional initializing expression follows
+ boolean fld_final = false; // Field is final, but not deeply final
+ boolean uninit = false; // Required non-null constructor field
+ Node expr;
+ if( P.match("=") ) {
+ if( isExternDecl() ) {
+ if( !(t instanceof TypeFunPtr) ) {
+ Node ptr = externData(name,t);
+ // The lexical binding is a fixed address; its contents are mutable.
+ if( !P._scope.define(name,t,true,ptr,loc) )
+ throw Parser.error("Redefining name '"+name+"'",loc);
+ P._scope.lookup(name)._extern = P._code.externAlias(name);
+ return P._code.ZERO;
+ }
+ expr = externDecl(name,t);
+ t = expr._type; // Upgrade declared type to the exact extern decl type
+ } else if( P.structs.isConstructorDecl(name) ) {
+ expr = P.functions.parseConstructor(name,false);
+ } else {
+ expr = P.expressions.parseAssignment();
+ }
+ // var/val, then type comes from expression
+ if( inferType ) {
+ if( expr._type==Type.NIL )
+ throw P.error("a not-null/non-zero expression");
+ t = expr._type;
+ if( !xfinal ) t = Conversions.mutableInferredType(t);
+ // Instance fields may change until the constructor finishes.
+ // Keep the declared family/access, not the default's constant
+ // value or string contents. Class constants remain static.
+ else if( P._scope.klast() instanceof Kind.Func fun && FunNode.isInstance(fun._name) && !(t instanceof TypeFunPtr) )
+ t = P.conversions.constructorFieldType(t);
+ }
+
+ // expr is a constant function
+ if( t instanceof TypeFunPtr decl && expr._type instanceof TypeFunPtr tfp && tfp.isConstant() ) {
+ t = decl.makeFrom(tfp.fidx());
+ FunNode fun = P._code.link(tfp);
+ if( fun != null )
+ fun.setName(name); // Assign debug name to Simple function
+ }
+
+ } else {
+ // Since no initializer, not-final but might need an initializer
+
+ // Need an expression to infer the type.
+ // Also, if not-null then need an initializing expression.
+ // Allowed in a class def, because type/init will happen in the constructor.
+ if( (inferType || (t instanceof TypeNil tn && !tn.nullable() )) && !P._scope.inConstructor() )
+ throw P.errorSyntax("=expression");
+ // Initial value for uninitialized struct fields.
+ expr = switch( t ) {
+ // Required pointers retain their declared shape, but include
+ // null until every constructor path assigns them.
+ case TypeNil tn -> tn.nullable() ? P.con(Type.NIL) : P.con(t.meet(Type.NIL));
+ case TypeInteger ti -> xfinal ? P.con(Type.BOTTOM) : P._code.ZERO;
+ case TypeFloat tf -> P.con(xfinal ? Type.BOTTOM : TypeFloat.FZERO);
+ // Bottom signals type inference: they must be initialized in
+ // the constructor and that's when we'll discover the type.
+ case Type tt -> { assert tt==Type.BOTTOM; yield P.con(tt); }
+ };
+ // Every non-null pointer without a default is constructor-required.
+ // Without `!` it is also shallow-final; mutability and definite
+ // initialization are independent properties.
+ if( (xfinal && !(t instanceof TypeNil)) || (t instanceof TypeNil tn && !tn.nullable()) ) {
+ uninit = true;
+ if( xfinal ) fld_final = true;
+ }
+ }
+
+ // Lift expression, based on type
+ // Do not let an authoritative Convert hide the nullable
+ // not-yet-initialized state from constructor-local type flow.
+ Node lift = uninit ? expr : P.conversions.convertExpr(expr, t);
+
+ // Define a new name
+ if( !P._scope.define(name,t,xfinal || fld_final,lift, loc) )
+ throw P.error("Redefining name '" + name + "'", loc);
+ P.structs.declareField(P._scope.lookup(name),lift,uninit);
+ return lift;
+ }
+
+ // Just after parsing "type foo = " can parse `"C"`
+ private boolean isExternDecl( ) {
+ int old = P.pos();
+ String s = P._lexer.parseString();
+ if( "C".equals(s) ) return true;
+ P.pos(old);
+ return false;
+ }
+ // External linked constant
+ private ConstantNode externDecl( String ex, Type t ) {
+ if( t instanceof TypeFunPtr tfp ) { // Generic TFP from type parse
+ int fidx = P._code.fidx(P._ref._cname); // Get a FIDX
+ tfp = tfp.makeFrom(fidx); // Become a constant
+ P._code.externFunc(fidx,ex); // Map fidx to extern name
+ t = tfp;
+ }
+ return (ExternNode)(new ExternNode(t,ex).peephole());
+ }
+
+ // A lexical C binding has a singleton namespace with one external field.
+ // The namespace occupies no storage; its field offset supplies the address.
+ private Node externData(String name, Type t) {
+ if( !(t instanceof TypeInteger) && !(t instanceof TypeFloat) )
+ throw P.error("C data binding requires an integer or float type");
+ String cname = ("C:"+name).intern();
+ TypeStruct ts = TypeStruct.make(cname,false,Field.make(name,t,P._code.externAlias(name),false,true));
+ Type old = Parser.TYPES.putIfAbsent(cname,ts);
+ if( old!=null && old!=ts )
+ throw P.error("Conflicting types for C data '"+name+"'");
+ return P.con(TypeMemPtr.make((byte)2,ts,true));
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Expressions.java b/src/main/java/com/seaofnodes/simple/parser/Expressions.java
new file mode 100644
index 00000000..93c07d8a
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/Expressions.java
@@ -0,0 +1,394 @@
+package com.seaofnodes.simple.parser;
+
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.parser.Parser.ParseException;
+import com.seaofnodes.simple.type.*;
+
+import static com.seaofnodes.simple.util.Utils.TODO;
+
+/** Recursive-descent expression precedence, from assignment down to primaries. */
+final class Expressions {
+ private final Parser P;
+
+ Expressions(Parser P) { this.P = P; }
+
+ /** Parse: [name '='] expr
+ */
+ Node parseAssignment() {
+ int old = P.pos();
+ String name = P._lexer.matchId();
+ // Just a plain expression, no assignment.
+ // Distinguish `var==expr` from `var=expr`
+ if( name==null || Parser.KEYWORDS.contains(name) || !P.matchOpx('=','=') )
+ { P.pos(old); return parseExpression(); }
+
+ // Find variable to update
+ Var def = P._scope.lookup(name);
+ if( def==null )
+ throw P.error("Undefined name '" + name + "'");
+
+ if( def._extern!=0 ) // Parse the C binding as a memory lvalue
+ { P.pos(old); return parseExpression(); }
+
+ P.references.checkAssignment(def);
+ return P.references.assign(def,parseAssignment());
+ }
+
+ /**
+ * Parse an expression of the form:
+ *
+ * <pre>
+ * expression : logicalExpression ('?' assignment (':' assignment)?)?
+ * </pre>
+ * @return an expression {@link Node}, never {@code null}
+ */
+ private Node parseExpression() {
+ Node expr = parseLogical();
+ return P.match("?") ? P.controlflow.conditional(expr,":") : expr;
+ }
+
+ /**
+ * Parse a logical expression
+ *
+ * <pre>
+ * logicalExpression : bitwiseExpression (('&&' | '||') logicalExpression)?
+ * </pre>
+ * @return a bitwise expression {@link Node}, never {@code null}
+ */
+
+ Node parseLogical() {
+ Node lhs = parseBitwise();
+ while (true) {
+ if( P.match("&&") ) lhs = P.controlflow.conditional(lhs, "&&");
+ else if( P.match("||") ) lhs = P.controlflow.conditional(lhs, "||");
+ else break;
+ }
+ return lhs;
+ }
+
+ private Node parseBitwise() {
+ Node lhs = parseEquality();
+ while( true ) {
+ if( P.matchOp('&') ) lhs = new AndNode(P.loc(),lhs,null);
+ else if( P.matchOp('|') ) lhs = new OrNode(P.loc(),lhs,null);
+ else if( P.match ("^") ) lhs = new XorNode(P.loc(),lhs,null);
+ else break;
+ lhs.setDef(2,parseEquality());
+ lhs = P.peep(lhs);
+ }
+ return lhs;
+ }
+
+ /**
+ * Parse an eq/ne expression
+ */
+ private Node parseEquality() {
+ Node lhs = parseComparison();
+ while( true ) {
+ boolean eq = false;
+ if( P.match("==") ) eq = true;
+ else if( !P.match("!=") ) break;
+ lhs.keep();
+ Node rhs = parseComparison();
+ lhs = P.peep(new BoolNode.EQ(lhs.unkeep(),rhs));
+ if( !eq ) lhs = P.peep(new NotNode(lhs));
+ }
+ return lhs;
+ }
+
+ /**
+ * Parse an expression of the form:
+ *
+ * <pre>
+ * expr : shiftExpr < shiftExpr <= shiftExpr...
+ * expr : shiftExpr > shiftExpr >= shiftExpr...
+ * </pre>
+ * @return an comparator expression {@link Node}, never {@code null}
+ */
+ private Node parseComparison() {
+ Node lhs = parseShift();
+ int dir = parseCompDir();
+ // No compare
+ if( dir==0 ) return lhs;
+ lhs.keep();
+
+ // Compare
+ Node rhs = parseShift().keep();
+ Node cmp = makeCompBool(dir,lhs.unkeep(),rhs).keep(); // Convert to a bool
+
+ // Stacked compares?
+ int dir0 = parseCompDir();
+ if( dir0 == 0 ) {
+ rhs.unkill();
+ return cmp.unkeep();
+ }
+
+ // Keep the middle operand separately from the boolean result.
+ // The next test compares adjacent operands, not that boolean.
+ Node ifNode = new IfNode(P.ctrl(), cmp.unkeep()).peephole();
+ Node ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep();
+ Node ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole();
+ // False side does nothing but capture memory & side-effects
+ ScopeNode fail = P._scope.dup();
+ fail.ctrl(ifF);
+
+ // Loop over stacked compares. Each loop moves the old RHS to become
+ // the new LHS, so e.g. (a < b < c < d) compares (LHS:a < RHS:b) then
+ // compares (LHS:b < RHS:c) then (LHS:c < RHS:d), etc.
+ while( dir0 != 0 ) {
+ if( Math.abs(dir) != Math.abs(dir0) )
+ throw P.error("Mixing relational directions in a chained relational test");
+ // True side parses next arm of test
+ P.ctrl(ifT.unkeep());
+ lhs = rhs; // Reuse the previous RHS exactly once
+ rhs = parseShift().keep();
+ cmp = makeCompBool(dir0,lhs.unkeep(),rhs); // Convert to a bool
+ ifNode = new IfNode(P.ctrl(), cmp).peephole();
+ ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep();
+ ifF = new CProjNode(ifNode.unkeep(), 1, "False").peephole();
+ // Merge result into the fail case
+ P.ctrl(ifF);
+ fail.mergeScopes(P._scope.dup(), P.loc()).peephole();
+ // Check next stacked compare
+ dir0 = parseCompDir();
+ }
+ rhs.unkill();
+ P.ctrl(ifT.unkeep());
+ RegionNode r = fail.mergeScopes(P._scope, P.loc()).init();
+ P._scope = fail;
+ return new PhiNode("", r,P.con(0),P.con(1)).peephole();
+ }
+
+ private int parseCompDir() {
+ if( P.match("<=") ) return -1; // Negative for "equals-or-less"
+ if( P.match(">=") ) return -2; // 1 for <, 2 for >
+ if( P.match("<" ) ) return 1; // Positive for just "less"
+ if( P.match(">" ) ) return 2;
+ return 0;
+ }
+
+ private Node makeCompBool( int dir, Node lhs, Node rhs ) {
+ if( Math.abs(dir)==2 )
+ { Node tmp = lhs; lhs = rhs; rhs = tmp; }
+ Node cmp = dir < 0 ? new BoolNode.LE(lhs,rhs) : new BoolNode.LT(lhs,rhs);
+ return P.peep(cmp);
+ }
+
+ /**
+ * Parse an additive expression
+ *
+ * <pre>
+ * shiftExpr : additiveExpr (('<<' | '>>' | '>>>') additiveExpr)*
+ * </pre>
+ * @return a shift expression {@link Node}, never {@code null}
+ */
+ private Node parseShift() {
+ Node lhs = parseAddition();
+ while( true ) {
+ if( P.match("<<") ) lhs = new ShlNode(P.loc(),lhs,null);
+ else if( P.match(">>>")) lhs = new ShrNode(P.loc(),lhs,null);
+ else if( P.match(">>") ) lhs = new SarNode(P.loc(),lhs,null);
+ else break;
+ lhs.setDef(2,parseAddition());
+ ParseException err;
+ if( (err = lhs.err()) != null )
+ throw err;
+ lhs = P.peep(lhs);
+ }
+ return lhs;
+ }
+
+ /**
+ * Parse an additive expression
+ *
+ * <pre>
+ * additiveExpr : multiplicativeExpr (('+' | '-') multiplicativeExpr)*
+ * </pre>
+ * @return an add expression {@link Node}, never {@code null}
+ */
+ private Node parseAddition() {
+ Node lhs = parseMultiplication();
+ while( true ) {
+ if( P.match("+") ) lhs = new AddNode(lhs,null);
+ else if( P.match("-") ) lhs = new SubNode(lhs,null);
+ else break;
+ lhs.setDef(2,parseMultiplication());
+ lhs = P.peep(lhs);
+ }
+ return lhs;
+ }
+
+ /**
+ * Parse an multiplicativeExpr expression
+ *
+ * <pre>
+ * multiplicativeExpr : unaryExpr (('*' | '/') unaryExpr)*
+ * </pre>
+ * @return a multiply expression {@link Node}, never {@code null}
+ */
+ private Node parseMultiplication() {
+ var lhs = parseUnary();
+ while( true ) {
+ if( P.match("*") ) lhs = new MulNode(lhs,null);
+ else if( P.match("/") ) lhs = new DivNode(lhs,null);
+ else break;
+ lhs.setDef(2,parseUnary());
+ lhs = P.peep(lhs);
+ }
+ return lhs;
+ }
+
+ /**
+ * Parse a unary minus expression.
+ *
+ * <pre>
+ * unaryExpr : ('-') unaryExpr | '!') unaryExpr | postfixExpr | primaryExpr | '--' Id | '++' Id
+ * </pre>
+ * @return a unary expression {@link Node}, never {@code null}
+ */
+ private Node parseUnary() {
+ // Pre-dec/pre-inc
+ int old = P.pos();
+ if( P.match("--") || P.match("++") ) {
+ int delta = P._lexer.peek(-1)=='+' ? 1 : -1; // Inc vs Dec
+ int start = P.pos();
+ String name = P._lexer.matchId();
+ Var var = name==null ? null : P._scope.lookup(name);
+ if( var!=null && !(var.type() instanceof TypeMemPtr) ) {
+ if( var._extern==0 && var._final && !P._scope.canInit(var) )
+ throw P.error("Cannot reassign final '"+var._name+"'");
+ // Resolve fields just as a read or postfix update does. Updating
+ // their declaration's local slot would silently lose the Store.
+ P.pos(start);
+ return parsePrimary(delta);
+ }
+ // Reset, try again
+ P.pos(old);
+ }
+ if (P.match("-")) return P.peep(new MinusNode(parseUnary()));
+ if (P.match("!")) return P.peep(new NotNode(parseUnary()));
+ // Else be a primary expression
+ return parsePrimary();
+ }
+
+ /**
+ * Parse a primary expression:
+ *
+ * <pre>
+ * primaryExpr : integerLiteral | "string" | 'char' | true | false | null |
+ * new Type | '(' expression ')' | Id['++','--'] |
+ * type['.' Id]*
+ * </pre>
+ * @return a primary {@link Node}, never {@code null}
+ */
+ private Node parsePrimary() { return parsePrimary(0); }
+
+ // pre is +/-1 for a prefix update of a known name, zero otherwise.
+ private Node parsePrimary(int pre) {
+ if( P._lexer.isNumber(P._lexer.peek()) ) return parseLiteral();
+ if( P._lexer.peek('"') ) return parsePostfix(P.allocations.newString(P._lexer.parseString()));
+ if( P.matchx("true" ) ) return P.con(1);
+ if( P.matchx("false") ) return P._code.ZERO;
+ if( P.matchx("null" ) ) return P.con(Type.NIL);
+ if( P.match ("'" ) ) return parseChar();
+ if( P.match ("(" ) ) return parsePostfix(P.require(parseAssignment(), ")"));
+ if( P.matchx("new" ) ) return parsePostfix(P.allocations.parseNew());
+ if( P.match ("{" ) ) return parsePostfix(P.require(P.functions.parseFunction(),"}"));
+
+ int pos = P.pos();
+ // Expect an identifier now
+ String id = P._lexer.matchId();
+ if( id == null || Parser.KEYWORDS.contains(id) )
+ throw P.errorSyntax("an identifier or expression");
+ int pos2 = P.pos();
+ return P.references.parseName(id,pre,pos,pos2);
+ }
+
+ // Check for assign-update, "x += e0".
+ // Returns "x + e0"; the caller assigns value.
+ // if ch is postfix, then lhs is kept and caller will unkeep and return it.
+ Node opAssign( char ch, Node lhs, Type t ) {
+ // RHS of the update.
+ lhs.keep(); // Alive across parseAssignment
+ Node rhs =
+ (byte)ch == 1 ? P.con( 1) : // var++
+ (byte)ch == -1 ? P.con(-1) : // var--
+ parseAssignment(); // var op= rhs
+ if( !Lexer.postfix(ch) ) lhs.unkeep(); // Allow to die in next peep
+ // 4 cases:
+ // int + int ==>> narrow int
+ // int + flt ==>> error, caller must fail assigning flt into int
+ // flt + int ==>> use float op, wrap toFloat()
+ // flt + flt ==>> use float op
+ Node op = switch(ch) {
+ case 1, (char)-1,
+ '+' -> new AddNode(lhs,rhs);
+ case '-' -> new SubNode(lhs,rhs);
+ case '*' -> new MulNode(lhs,rhs);
+ case '/' -> new DivNode(lhs,rhs);
+ case '|' -> new OrNode(null,lhs,rhs);
+ case '&' -> new AndNode(null,lhs,rhs);
+ case '^' -> new XorNode(P.loc(),lhs,rhs);
+ case Lexer.SHL -> new ShlNode(P.loc(),lhs,rhs);
+ case Lexer.SAR -> new SarNode(P.loc(),lhs,rhs);
+ case Lexer.SHR -> new ShrNode(P.loc(),lhs,rhs);
+ default -> throw TODO();
+ };
+ // Convert to float ops, or narrow int types; error if not declared type.
+ // Also, if postfix LHS is still keep()
+ return P.conversions.convertExpr(P.peep(op),t);
+ }
+
+ /**
+ * Parse postfix expression; this can be a field expression, an array
+ * lookup or a postfix operator like '#'
+ *
+ * <pre>
+ * expr ('.' FIELD)* [ = expr ] // Field reference
+ * expr '#' // Postfix unary read operator
+ * expr ['++' | '--' ] // Postfix unary write operator
+ * expr ('[' expr ']')* = [ = expr ] // Array reference
+ * expr '(' [args,]* ')' // Function call
+ * </pre>
+ */
+ Node parsePostfixMethod(Node expr, Node self) {
+ // TODO: This is too broad because the parser lacks strong types here.
+ // Some normal functions can end up with a "self" argument.
+
+ // Self method call? Self is a TMP-not-CLZ, then assume and pass self.
+ if( P.match("(") ) {
+ expr = parsePostfix(P.require(P.functions.functionCall(expr,self),")"));
+ } else if( expr != self )
+ self.isKill(); // Not a method call, no need for self
+ return parsePostfix(expr);
+ }
+ Node parsePostfix(Node expr) {
+ P._lexer.skipWhiteSpace();
+ Lexer site = P.loc();
+ String name;
+ if( P.match(".") ) name = P.requireId();
+ else if( P.match("#") ) name = "#";
+ else if( P.match("[") ) name = "[]";
+ // Can get here without an explicit receiver: "f()" or
+ // "self.method()(next)(next)(next)". Pass the current function self.
+ else if( P.match("(") ) return parsePostfix(P.require(P.functions.functionCall(expr,P.references.defaultSelf()),")"));
+ else return expr; // No postfix
+
+ return P.references.parseField(expr,name,0,site);
+ }
+
+ /**
+ * Parse integer literal
+ *
+ * <pre>
+ * integerLiteral: [1-9][0-9]* | [0]
+ * floatLiteral: [digits].[digits]?[e [digits]]?[f]?
+ * </pre>
+ */
+ private ConstantNode parseLiteral() { return P.con(P._lexer.parseNumber()); }
+
+ // Already parsed "'"
+ Node parseChar() {
+ return P.require(P.con(TypeInteger.constant(P._lexer.parseChar())),"'");
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Functions.java b/src/main/java/com/seaofnodes/simple/parser/Functions.java
new file mode 100644
index 00000000..14dee33b
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/Functions.java
@@ -0,0 +1,349 @@
+package com.seaofnodes.simple.parser;
+
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.node.ScopeNode.Kind;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+import java.util.BitSet;
+
+import static com.seaofnodes.simple.util.Utils.TODO;
+
+/** Function expressions, parameters, calls, and function entry/exit graphs. */
+final class Functions {
+ private final Parser P;
+ // Merge all function exits here.
+ ScopeNode _returnScope;
+
+ Functions(Parser P) { this.P = P; }
+
+ /**
+ * Parse a function body; the caller will parse the surrounding "{}"
+ *
+ * <pre>
+ * { [type arg,]* -> expr }
+ * { -> statements } // The no-argument function
+ * </pre>
+ */
+ Node parseFunction() {
+ Ary<Type> ts = new Ary<>(Type.class);
+ Ary<String> ids = new Ary<>(String.class);
+ BitSet finals = new BitSet();
+ Type self = P.types.implicitSelfType();
+ ts.push(self);
+ ids.push(self == TypePtr.PTR ? "#self" : "self");
+
+ // Parse other arguments
+ P._lexer.skipWhiteSpace();
+ Lexer loc = P.loc(); // First argument location
+ parameters(ts,ids,finals,false);
+ // Make a concrete function type, with a fidx
+ TypeFunPtr tfp = TypeFunPtr.make1((byte)2,true,ts.asAry(),Type.BOTTOM,P._code.fidx(P._ref._cname));
+ ReturnNode ret = parseFunctionBody(tfp,loc,finals,ids.asAry());
+ return new FunPtrNode(tfp,P._code._start,ret).peephole();
+ }
+
+ Node parseConstructor(String name, boolean explicit) {
+ P.require("{");
+ Ary<Type> ts = new Ary<>(Type.class);
+ Ary<String> ids = new Ary<>(String.class);
+ BitSet finals = new BitSet();
+ TypeStruct self = P._scope.constructorSelf();
+ assert self != null;
+ ts.push(TypeMemPtr.make(Structs.constructorRecv(self)).withPartial(true));
+ ids.push("self");
+ // Explicit constructors use the same hidden allocation arguments as
+ // <init>: public memory is the call memory edge (index 1), followed by
+ // self (index 2) and the private, unescaped object memory (index 3).
+ ts.push(TypeMem.makePrivate(Type.BOTTOM));
+ ids.push("#selfMem");
+
+ P._lexer.skipWhiteSpace();
+ Lexer loc = P.loc();
+ parameters(ts,ids,finals,explicit);
+ TypeFunPtr tfp = TypeFunPtr.make1((byte)2,true,ts.asAry(),TypeMem.makePrivate(Type.BOTTOM),P._code.fidx(P._ref._cname));
+ Node[] preParse = P._scope.save();
+ ReturnNode ret = parseBody((self._name+"."+name).intern(),tfp,loc,finals,ids.asAry());
+ P._scope.restore(preParse);
+ P.require("}");
+ return new FunPtrNode(tfp,P._code._start,ret).peephole();
+ }
+
+ // parameterList followed by the function arrow; callers seed hidden parameters.
+ private void parameters(Ary<Type> ts, Ary<String> ids, BitSet finals, boolean constructor) {
+ while( true ) {
+ Type t = P.types.parseType(); // Arg type
+ if( t==null ) break;
+ if( P.match("^") ) {
+ if( !(t instanceof TypeMemPtr ptr) || ptr._obj.isAry() )
+ throw P.error("'^' requires a struct parameter");
+ t = ptr.withPartial(true);
+ P.construction._borrowed = true;
+ }
+ finals.set(ids.size(),P.declarations.bindingFinal(t));
+ String id = P.requireId();
+ ts .push(t ); // Push type/arg pairs
+ ids.push(id);
+ P.match(",");
+ }
+ P.require("->");
+ }
+
+ /**
+ * Parse function call arguments; caller will parse the surrounding "()"
+ * <pre>
+ * ( arg* )
+ * </pre>
+ */
+ Node functionCall(Node fcn, Node self) {
+ Lexer site = P.loc();
+ site._position--; // Opening '('; arguments can contain nested calls.
+ if( fcn._type == Type.NIL )
+ throw P.error("Calling a null function pointer");
+ fcn.keep(); // Keep while parsing args
+
+ Ary<Node> args = new Ary<>( Node.class );
+ args.push(null); // Space for ctrl,mem
+ args.push(null);
+ if( self != null ) // Method call has a self
+ args.push(self.keep());
+ arguments(args);
+ // Control & memory after parsing args
+ args.set(0,P.ctrl().keep());
+ args.set(1,P.mem().keep());
+ args.push(fcn); // Function pointer
+ // Unkeep them all
+ for( Node arg : args )
+ arg.unkeep();
+ return functionCall(args,site);
+ }
+
+ Node functionCall( Ary<Node> args ) {
+ return functionCall(args,P.loc());
+ }
+
+ Node functionCall( Ary<Node> args, Lexer site ) {
+ // Dead into the call? Skip all the node gen
+ if( P.ctrl()._type == Type.XCONTROL ) {
+ for( Node arg : args )
+ if( arg.isUnused() )
+ arg.kill();
+ return P.con(Type.TOP);
+ }
+
+ // Into the call
+ args.set(1,P.construction.callMemory(args,args.at(1)));
+ CallNode call = (CallNode)new CallNode(site, args.asAry()).peephole();
+ // Post-call setup
+ CallEndNode cend = (CallEndNode)new CallEndNode(call,TypeRPC.constant(P._code.rpc(P._ref._cname))).peephole();
+ call.peephole(); // Rerun peeps after CallEnd, allows early inlining
+ cend = (CallEndNode)cend.keep().peephole(); // TODO: Might inline
+ // Control from CallEnd
+ P.ctrl(new CProjNode(cend,0,ScopeNode.CTRL).peephole());
+ // Memory from CallEnd
+ Node mem = new ProjNode(cend,1,ScopeNode.MEM0).peephole();
+ P.mem(mem);
+ // Call result
+ return new ProjNode(cend.unkeep(),2,"#2").peephole();
+ }
+
+ // argumentList; returns kept values and leaves the closing parenthesis to the caller.
+ void arguments(Ary<Node> args) {
+ while( !P.peek(')') ) {
+ Node arg = P.expressions.parseAssignment();
+ if( arg==null ) break;
+ args.push(arg.keep());
+ if( !P.match(",") ) break;
+ }
+ }
+
+ /**
+ * Parses a function body, assuming the header is parsed.
+ */
+
+ private ReturnNode parseFunctionBody( TypeFunPtr sig, Lexer loc, BitSet finals, String[] ids) {
+ // Record & restore the existing scope vars. The function parse might
+ // update fields, so when the parse is done reset back to the pre-parse
+ // state.
+ Node[] preParse = P._scope.save();
+
+ // Parse function body normally
+ ReturnNode ret = parseBody(null, sig,loc,finals,ids);
+
+ // Reset all fields to pre-parse days
+ P._scope.restore(preParse);
+ return ret;
+ }
+
+ // Parse a function body
+
+ // Stack the {break,continue,return} scopes and a new lexical scope.
+ // Build a FunNode and parameters, extending scope by parm names.
+ // Parse the function body
+ // - while( not end-of-function )
+ // - - parseStatement
+ // - - If isInit, also incrementally extend the self-type.
+ // If isInit
+ // - upgrade and close the self-type
+ // - Store class singleton fields; instance Stores already exist
+ // - <init> and user constructors return their current private memory
+ // Unstack the {break,continue,return} scopes.
+
+ ReturnNode parseBody( String funName, TypeFunPtr sig, Lexer loc, String[] ids) {
+ return parseBody(funName, sig, loc, null, ids);
+ }
+
+ ReturnNode parseBody( String funName, TypeFunPtr sig, Lexer loc, BitSet finals, String[] ids) {
+ Structs.Constructor outerCtor = P.structs._pendingCtor;
+ P.structs._pendingCtor = null;
+ boolean isInit = FunNode.isInit(funName);
+ boolean isCtor = isInit || P.structs.isExplicitConstructorName(funName);
+ String typeName = isCtor ? ((TypeMemPtr)sig._sig[0])._obj._name : null;
+
+ // Stack parser state on the local Java stack, and unstack it later
+ P._scope.push(new Kind.Func(funName,isCtor && !funName.endsWith(".<clinit>")));
+ ScopeNode breakScope = P.statements._breakScope; P.statements._breakScope = null;
+ ScopeNode continueScope = P.statements._continueScope; P.statements._continueScope = null;
+ ScopeNode returnScope = _returnScope; _returnScope = null;
+
+ int oldUID = P._code.UID(); // Used to approximate function size
+ FunNode fun = enterFunction(funName,sig,loc,finals,ids);
+ Construction.Frame outerConstruction = P.construction.enter(fun,isCtor && !fun.isClz());
+ Node rpc = fun.parm(0);
+ if( fun.isClz() ) P.structs.beginClassInit(fun,loc);
+
+ // Parse the function body.
+ Node last = P._code.ZERO; // Last statement as the default result
+
+ int nvar = 0;
+ while (!P.peek('}') && !P._lexer.isEOF()) {
+ // Parse a statement; record last statement as default return
+ last = P._code.add(P.statements.parseStatement());
+ // For <init> and <clinit>, local vars are really struct fields and
+ // get exposed as discovered. This allows late local functions to
+ // use early declared fields.
+ if( isInit )
+ nvar = P.structs.updateSelfAsFieldsDiscovered( typeName, nvar );
+ }
+
+ if( isInit ) P.structs.finishConstructor();
+ P.structs._pendingCtor = outerCtor;
+
+ ParmNode self = fun.parm(2);
+ // Last expression is the return
+ if( P.ctrl()._type==Type.CONTROL ) {
+ // Constructors get special return types
+ if( fun.isClz() ) {
+ assert last!=null;
+ } else if( isCtor ) {
+ last = P.construction.selfMem();
+ }
+ addReturn(last);
+ // exit path is now dead (nothing live after a "return")
+ }
+ if( isInit )
+ P.structs.finishInitializer(typeName,fun.isClz(),self);
+
+ // Build a return from the _returnScope.
+ // Can be no returns for never-exit functions
+ Node rctl = _returnScope==null ? P._code.XCTRL : _returnScope.ctrl().peephole();
+ Node rmem = rctl==P._code.XCTRL ? P.con(TypeMem.TOP) : _returnScope.mem();
+ Node expr = rctl==P._code.XCTRL ? P.con(Type.TOP) : _returnScope._inputs.last().peephole();
+ ReturnNode ret = (ReturnNode)P.peep(new ReturnNode(rctl, rmem, expr, rpc, fun));
+ fun.setRet(ret);
+ // Approximate function size, for inlining heuristics
+ fun._approxUIDs = P._code.UID() - oldUID;
+ for( Node cend : ret.outs() )
+ if( cend instanceof CallEndNode )
+ throw TODO(); //_code.add(cend); // have call ends ALREADY? just barely made the Return
+
+ // Export (and keep-alive) functions until prove dead
+ P._ref.addFun(P._code,fun);
+
+ // Unstack parser state
+ if( _returnScope != null ) _returnScope.kill();
+ _returnScope = returnScope;
+ P.statements._continueScope = continueScope;
+ P.statements._breakScope = breakScope;
+ P._scope.pop();
+ P.construction._frame = outerConstruction;
+
+ return ret;
+ }
+
+ private FunNode enterFunction(String funName, TypeFunPtr sig, Lexer loc, BitSet finals, String[] ids) {
+ FunNode fun = (FunNode)P.peep(new FunNode(P.loc(),sig, funName, P._ref, null, P._ref._start));
+ // Once the function header is available, install in linker table -
+ // allowing recursive functions. Linker matches on declared args and
+ // exact fidx, and ignores the return (because the fidx will only match
+ // the exact single function).
+ P._code.link(fun);
+
+ new ParmNode("$rpc",0,TypeRPC.BOT,fun,ConstantNode.seed(TypeRPC.BOT).peephole()).peephole();
+
+ // Pre-call the function from Start, with worse-case arguments. This
+ // represents all the future, yet-to-be-parsed functions calls and
+ // external calls.
+ P._scope.ctrl(fun); // Scope control from function
+ // Private mem alias tracking per function
+ Node defaultMem = new ProjNode(P._ref._start,1,ScopeNode.MEM0).peephole();
+ Node privMem = new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun,defaultMem).peephole();
+ P.mem(privMem);
+ // All args, "as-if" called externally
+ for( int i = 0; i<ids.length; i++ ) {
+ Type t = sig.arg(i);
+ // Args take a default input if called from Start/external-world
+ Node parm = new ParmNode(ids[i],i+2,t,fun,ConstantNode.seed(fun.defaultArg(i)).peephole()).peephole();
+ if( t instanceof TypeMemPtr ptr && ptr._partial ) parm = new ConstructNode(parm).peephole();
+ P._scope.define(ids[i], t, (i==0 && ids[0]=="self") || (finals!=null && finals.get(i)), parm, loc);
+ }
+
+ return fun;
+ }
+ Node addReturn(Node expr) {
+ if( P.construction._frame._owner ) expr = P.construction.selfMem();
+ expr.keep();
+ // Force the pending lazy bulk-memory Phi.
+ P.mem(); // Force a pending lazy bulk-memory Phi
+ // Class fields are still stored at exit. Instance fields already
+ // travel in expr's private memory; only their scope layout is retained.
+ int lexN = P._scope.enclosingFuncOrDecl();
+ Kind.Func k = (Kind.Func)P._scope._kinds.at(lexN);
+ if( FunNode.isInit(k._name) ) {
+ int nestedLexSize = lexN+1 < P._scope.depth() ? P._scope._kinds.at(lexN+1)._lexSize : P._scope.nIns();
+ if( k._name.endsWith(".<clinit>") )
+ for( int i = k._lexSize; i<nestedLexSize; i++ )
+ P._scope.update(P._scope.var(i),null);
+ if( _returnScope != null ) {
+ Var vexpr = _returnScope._vars.pop();
+ Node oldX = _returnScope.removeLast();
+ int rlen = _returnScope.nIns();
+ while( rlen < nestedLexSize ) {
+ _returnScope._vars.add(P._scope.var(rlen));
+ _returnScope.addDef(P.con(P._scope.var(rlen++).type().makeZero()));
+ }
+ _returnScope._vars.add(vexpr);
+ _returnScope.addDef(oldX);
+ }
+ }
+
+ // No prior merge point? Just clone and hang on to it
+ if( _returnScope == null ) {
+ _returnScope = P._scope.dup();
+ while( lexN+1 < _returnScope.depth() )
+ _returnScope._pop(); // Pop a nested block scope until we hit the function scope
+ _returnScope.define("$expr", Conversions.inferredReturnType(expr._type), true, expr, null);
+
+ } else {
+ // The initializer scopes have already been aligned above.
+ int rlen = _returnScope.nIns()-1;
+ RegionNode r = P.ctrl(new RegionNode(null, null,_returnScope.ctrl(), P._scope.ctrl()).init().keep());
+ _returnScope._merge(P._scope, r, rlen);
+ Node oldExpr = _returnScope.in(rlen);
+ _returnScope.setDef(rlen,PhiNode.make("$expr",expr._type, r, oldExpr, expr).peephole());
+ P._code.add(r);
+ _returnScope.ctrl(r.unkeep());
+ }
+ P.ctrl(P._code.XCTRL); // Kill control
+ return expr.unkeep();
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Lexer.java b/src/main/java/com/seaofnodes/simple/parser/Lexer.java
new file mode 100644
index 00000000..ec88738b
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/Lexer.java
@@ -0,0 +1,389 @@
+package com.seaofnodes.simple.parser;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.codegen.CompUnit;
+import com.seaofnodes.simple.codegen.Serialize;
+import com.seaofnodes.simple.parser.Parser.ParseException;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.BAOS;
+import java.util.HashMap;
+
+// Lexer provides low level access to the raw file bytes, peeks and matches
+// short strings, parses numbers, skips comments and whitespace, tracks
+// line numbers, allows the parse position to be saved and restored, and
+// serves as a location indicator for errors.
+
+public class Lexer {
+
+ // Input buffer; an array of text bytes read from a file or a string
+ private final byte[] _input;
+ // Original source, retained across inlining.
+ public final CompUnit _cu;
+ // Tracks current position in input buffer
+ int _position = 0;
+
+ //
+ private int _line_number = 1;
+ // Start of current line
+ private int _line_start = 0;
+
+ /** True when this location precedes {@code that} in the same source. */
+ public boolean before(Lexer that) {
+ return that != null && (_cu!=null ? _cu==that._cu : _input==that._input) && _position < that._position;
+ }
+
+ public String position() { return _line_number+":"+(_position-_line_start+1); }
+ public int pos() { return _position; }
+
+ /**
+ * Record the source text for lexing
+ */
+ public Lexer(String source) {
+ this(source.getBytes());
+ }
+
+ /**
+ * Direct from disk file source
+ */
+ public Lexer(byte[] buf) {
+ _input = buf;
+ _cu = null;
+ }
+
+ Lexer(CompUnit cu) {
+ _input = cu._src.getBytes();
+ _cu = cu;
+ }
+
+ // Imported location: source text is only needed when reporting an error.
+ Lexer(CompUnit cu, int pos) {
+ _input = null;
+ _cu = cu;
+ _position = pos;
+ }
+
+ public static void gather(Lexer loc, HashMap<String,Integer> strs) {
+ if( loc==null || loc._cu==null ) return;
+ Serialize.gather(strs,loc._cu._cname);
+ String file = loc._cu.sourceFile();
+ if( file!=null ) Serialize.gather(strs,file);
+ }
+
+ public static void packed(Lexer loc, BAOS out, HashMap<String,Integer> strs) {
+ if( loc==null || loc._cu==null ) { out.packed4(0); return; }
+ out.packed4(loc._position+1);
+ out.packed4(strs.get(loc._cu._cname));
+ String file = loc._cu.sourceFile();
+ out.packed4(file==null ? 0 : strs.get(file));
+ }
+
+ public static Lexer unpack(BAOS in, String[] strs) {
+ int pos = in.packed4();
+ if( pos==0 ) return null;
+ String owner = strs[in.packed4()];
+ String file = strs[in.packed4()];
+ CompUnit cu = CodeGen.CODE._compunits.get(owner.replace('.','/'));
+ assert cu!=null : owner;
+ if( !file.isEmpty() ) cu._sourceFile = file;
+ return new Lexer(cu,pos-1);
+ }
+
+ /**
+ * Copy a lexer from a lexer
+ */
+ Lexer(Lexer l) {
+ _input = l._input;
+ _cu = l._cu;
+ _position = l._position;
+ _line_number = l._line_number;
+ _line_start = l._line_start;
+ }
+
+ // Very handy in the debugger, shows the unparsed program
+ @Override
+ public String toString() {
+ return _input==null ? _cu._cname+"@"+_position : new String(_input, _position, _input.length - _position);
+ }
+
+ // True if at EOF
+ boolean isEOF() {
+ return _position >= _input.length;
+ }
+
+ // Peek next character, or report EOF
+ char peek() {
+ return isEOF() ? Character.MAX_VALUE // Special value that causes parsing to terminate
+ : (char) _input[_position];
+ }
+ // Just crash if misused
+ public byte peek(int off) { return _input[_position+off]; }
+
+ private void inc() {
+ if( _position++ < _input.length && _input[_position-1] == '\n' ) {
+ _line_number++;
+ _line_start = _position;
+ }
+ }
+ // Does not honor LF, so caller must roll back position on a LF
+ private char nextChar() {
+ char ch = peek();
+ inc();
+ return ch;
+ }
+
+ // True if a white space
+ private boolean isWhiteSpace() {
+ return peek() <= ' '; // Includes all the use space, tab, newline, CR
+ }
+
+ /**
+ * Return the next non-white-space character
+ */
+ void skipWhiteSpace() {
+ while( true ) {
+ if( isWhiteSpace() ) inc();
+ // Skip // to end of line
+ else if( _position+2 < _input.length &&
+ _input[_position ] == '/' &&
+ _input[_position+1] == '/') {
+ inc(); inc();
+ while( !isEOF() && _input[_position] != '\n' )
+ inc();
+ } else if( _position+2 < _input.length &&
+ _input[_position ] == '/' &&
+ _input[_position+1] == '*') {
+ // Skip /*comment*/
+ while( !isEOF() && !(_input[_position-1] == '*' && _input[_position] == '/'))
+ inc();
+ inc();
+ } else break;
+ }
+ }
+
+ // Return true, if we find "syntax" after skipping white space; also
+ // then advance the cursor past syntax.
+ // Return false otherwise, and do not advance the cursor.
+ boolean match(String syntax) {
+ assert syntax.indexOf('\n')==-1; // No newlines in match
+ skipWhiteSpace();
+ int len = syntax.length();
+ if (_position + len > _input.length) return false;
+ for (int i = 0; i < len; i++)
+ if ((char) _input[_position + i] != syntax.charAt(i))
+ return false;
+ _position += len;
+ return true;
+ }
+
+ // Match must be exact and not followed by more ID characters.
+ // Prevents identifier "ifxy" from matching an "if" statement.
+ boolean matchx(String syntax) {
+ if( !match(syntax) ) return false;
+ if( !isIdLetter(peek()) ) return true;
+ _position -= syntax.length();
+ return false;
+ }
+ // Match this char, and the next char must be different.
+ // Handles '&&' vs '&'
+ boolean matchOp( char c0 ) {
+ skipWhiteSpace();
+ if( _position+1 >= _input.length || _input[_position]!=c0 || _input[_position+1]==c0 )
+ return false;
+ inc();
+ return true;
+ }
+ // Match these two characters in a row
+ boolean matchOpx(char c0, char c1) {
+ skipWhiteSpace();
+ if( _position+1 >= _input.length || _input[_position]!=c0 || _input[_position+1]==c1 )
+ return false;
+ inc();
+ return true;
+ }
+
+ boolean peek(char ch) {
+ skipWhiteSpace();
+ return peek()==ch;
+ }
+
+ boolean peekIsId() {
+ skipWhiteSpace();
+ return isIdStart(peek());
+ }
+
+ // Return an identifier or null
+ String matchId() {
+ return peekIsId() ? parseId() : null;
+ }
+
+ // Used for errors
+ String getAnyNextToken() {
+ if (isEOF()) return "EOF";
+ if (isIdStart(peek())) return parseId();
+ if (isNumber(peek())) return parseNumberString();
+ if (isPunctuation(peek())) return parsePunctuation();
+ return String.valueOf(peek());
+ }
+
+ boolean isNumber(char ch) {return Character.isDigit(ch);}
+
+ // Return a constant Type, either TypeInteger or TypeFloat
+ Type parseNumber() {
+ int old = _position;
+ int len = isLongOrDouble();
+ boolean f32 = peek()=='f';
+ if( f32 ) nextChar();
+ int n = Math.abs(len);
+ if( f32 )
+ return TypeFloat.constant(Float.parseFloat(new String(_input,old,n)));
+ if( len > 0 ) {
+ if( len > 1 && _input[old]=='0' )
+ throw Parser.error("Syntax error: integer values cannot start with '0'",this);
+ long i = Long.parseLong(new String(_input,old,len));
+ return TypeInteger.constant(i);
+ }
+ return TypeFloat.constant(Double.parseDouble(new String(_input,old,n)));
+ }
+ private String parseNumberString() {
+ int old = _position;
+ int len = Math.abs(isLongOrDouble());
+ if( peek()=='f' ) {
+ nextChar();
+ len++;
+ }
+ return new String(_input,old,len);
+ }
+
+ // Return +len that ends a long
+ // Return -len that ends a double
+ private int isLongOrDouble() {
+ int old = _position;
+ char c;
+ while( Character.isDigit(c = nextChar()) ) ;
+ if( !(c=='e' || c=='.') )
+ return --_position - old;
+ if( peek() == '-' ) nextChar();
+ while( Character.isDigit(c = nextChar()) || c=='e' || c=='.' ) ;
+ return -(--_position - old);
+ }
+
+ // First letter of an identifier
+ private boolean isIdStart(char ch) {
+ return Character.isAlphabetic(ch) || ch == '_';
+ }
+
+ // All characters of an identifier, e.g. "_x123"
+ private boolean isIdLetter(char ch) {
+ return Character.isLetterOrDigit(ch) || ch == '_';
+ }
+
+ private String parseId() {
+ int start = _position;
+ while (isIdLetter(nextChar())) ;
+ return new String(_input, start, --_position - start).intern();
+ }
+
+ //
+ private boolean isPunctuation(char ch) {
+ return "=;[]<>()+-/*&|^".indexOf(ch) != -1;
+ }
+
+ private String parsePunctuation() {
+ int start = _position;
+ return new String(_input, start, 1);
+ }
+
+ // Next oper= character, or 0.
+ // As a convenience, mark "++" as a char 1 and "--" as char -1 (65535)
+ // Distinct tags for the multi-character compound operators.
+ static final char SHL = 2, SAR = 3, SHR = 4;
+
+ // ch is +/- 1, means oper++ or oper-- means postfix.
+ static boolean postfix(char ch) {
+ return (byte)ch== 1 || (byte)ch== -1;
+ }
+
+ public char matchOperAssign() {
+ skipWhiteSpace();
+ if( match("<<=" ) ) return SHL;
+ if( match(">>>=") ) return SHR;
+ if( match(">>=" ) ) return SAR;
+ if( _position+2 >= _input.length ) return 0;
+ char ch0 = (char)_input[_position];
+ if( "+-/*&|^".indexOf(ch0) == -1 ) return 0;
+ char ch1 = (char)_input[_position+1];
+ if( ch1 == '=' ) { _position += 2; return ch0; }
+ if( isIdLetter((char)_input[_position+2]) ) return 0;
+ if( ch0 == '+' && ch1 == '+' ) { _position += 2; return (char) 1; }
+ if( ch0 == '-' && ch1 == '-' ) { _position += 2; return (char)-1; }
+ return 0;
+ }
+
+ String parseString() {
+ if( !peek('"') ) return null;
+ inc();
+ StringBuilder sb = new StringBuilder();
+ while( !isEOF() ) {
+ char c = nextChar();
+ if( c=='"' ) return sb.toString();
+ if( c!='\\' ) {
+ sb.append(c);
+ continue;
+ }
+ if( isEOF() )
+ throw error("Unclosed string");
+ sb.append(escapedChar("string"));
+ }
+ throw error("Unclosed string");
+ }
+
+ private char escapedChar(String kind) {
+ char esc = nextChar();
+ return switch( esc ) {
+ case '0' -> '\u0000';
+ case 'n' -> '\n';
+ case 't' -> '\t';
+ case 'r' -> '\r';
+ case '\\' -> '\\';
+ case '"' -> '"';
+ case '\'' -> '\'';
+ default -> throw error("Unknown "+kind+" escape \\\\"+esc);
+ };
+ }
+
+ char parseChar() {
+ if( isEOF() ) throw error("Unclosed character");
+ char c = nextChar();
+ if( c=='\\' ) {
+ if( isEOF() ) throw error("Unclosed character");
+ c = escapedChar("character");
+ }
+ return c;
+ }
+
+ private ParseException error(String msg) { return Parser.error(msg,this); }
+
+ // Scan a deferred fragment, leaving its unmatched closing delimiter unread.
+ // Comments, quoted text, and escapes do not affect the nesting depth.
+ void skipTo(char open, char close, String what) {
+ int depth = 0;
+ while( true ) {
+ skipWhiteSpace();
+ if( isEOF() ) throw error("Unclosed "+what);
+ char ch = peek();
+ if( ch==close && depth==0 ) return;
+ nextChar();
+ if( ch==open ) depth++;
+ else if( ch==close ) depth--;
+ else if( ch=='"' || ch=='\'' ) {
+ char quote = ch;
+ while( !isEOF() ) {
+ ch = nextChar();
+ if( ch=='\\' ) nextChar();
+ else if( ch==quote ) break;
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Parser.java b/src/main/java/com/seaofnodes/simple/parser/Parser.java
new file mode 100644
index 00000000..4d140e55
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/Parser.java
@@ -0,0 +1,281 @@
+package com.seaofnodes.simple.parser;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.codegen.CompUnit;
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Stack;
+
+/** Source-unit entry point and shared cursor, scope, and IR utilities.
+ * Grammar productions live in the package-local components below. */
+public class Parser {
+ final Statements statements = new Statements(this);
+ final ControlFlow controlflow = new ControlFlow(this);
+ final Expressions expressions = new Expressions(this);
+ final Declarations declarations = new Declarations(this);
+ final Types types = new Types(this);
+ final Functions functions = new Functions(this);
+ final Structs structs = new Structs(this);
+ final Allocations allocations = new Allocations(this);
+ final References references = new References(this);
+ final Conversions conversions = new Conversions(this);
+ final Construction construction = new Construction(this);
+
+ public void checkConstructionSerializable() { construction.checkSerializable(); }
+
+ // Compile driver
+ public final CodeGen _code;
+
+ // The Lexer. Thin wrapper over a byte[] buffer with a cursor.
+ Lexer _lexer;
+
+ // Source file for compilation
+ public CompUnit _ref;
+
+ // Current classname being parsed
+ String _nestedType;
+ /**
+ * Current ScopeNode - ScopeNodes change as we parse code, but at any point of time
+ * there is one current ScopeNode. The reason the current ScopeNode can change is to do with how
+ * we handle branching. See {@link Statements#parseIf()}.
+ * <p>
+ * Each ScopeNode contains a stack of lexical scopes, each scope is a symbol table that binds
+ * variable names to Nodes. The top of this stack represents current scope.
+ * <p>
+ * We keep a list of all ScopeNodes so that we can show them in graphs.
+ * @see Statements#parseIf()
+ * @see #_xScopes
+ */
+ public ScopeNode _scope;
+
+ /**
+ * List of keywords disallowed as identifiers
+ */
+ public static final HashSet<String> KEYWORDS = new HashSet<>(){{
+ add("bool");
+ add("break");
+ add("byte");
+ add("continue");
+ add("else");
+ add("f32");
+ add("f64");
+ add("false");
+ add("flt");
+ add("i16");
+ add("i32");
+ add("i64");
+ add("i8");
+ add("if");
+ add("int");
+ add("new");
+ add("null");
+ add("return");
+ add("struct");
+ add("true");
+ add("u1");
+ add("u16");
+ add("u32");
+ add("u8");
+ add("while");
+ }};
+
+ /**
+ * We clone ScopeNodes when control flows branch; it is useful to have
+ * a list of all active ScopeNodes for purposes of visualization of the SoN graph
+ */
+ public final Stack<ScopeNode> _xScopes = new Stack<>();
+ // Layouts needed by field accesses in this source file. Resolve after
+ // parsing, since a later local struct declaration can satisfy the use.
+ public final LinkedHashSet<String> _requiredTypes = new LinkedHashSet<>();
+
+ // Mapping from a type name to a Type. The string name matches
+ // `type.str()` call.
+ public static final HashMap<String, Type> TYPES = new HashMap<>();
+ public static final HashMap<String, Type> INIT_TYPES =
+ new HashMap<>() {{
+ put("bool",TypeInteger.U1 );
+ put("byte",TypeInteger.U8 );
+ put("f32" ,TypeFloat .F32);
+ put("f64" ,TypeFloat .F64);
+ put("flt" ,TypeFloat .F64);
+ put("i16" ,TypeInteger.I16);
+ put("i32" ,TypeInteger.I32);
+ put("i64" ,TypeInteger.BOT);
+ put("i8" ,TypeInteger.I8 );
+ put("int" ,TypeInteger.BOT);
+ put("u1" ,TypeInteger.U1 );
+ put("u16" ,TypeInteger.U16);
+ put("u32" ,TypeInteger.U32);
+ put("u8" ,TypeInteger.U8 );
+ put("val" ,Type.TOP); // Marker type, indicates type inference
+ put("var" ,Type.BOTTOM); // Marker type, indicates type inference
+ }};
+
+ public static class ParseException extends RuntimeException {
+ public final Lexer _loc;
+ ParseException( String msg, Lexer loc ) { super(msg); _loc = loc; }
+ }
+
+ public Parser(CodeGen code ) {
+ _code = code;
+ }
+
+ public Ary<FRefNode> parse( CompUnit ref ) {
+ assert _scope == null && statements._breakScope == null && statements._continueScope == null && functions._returnScope == null;
+ assert references._frefs == null;
+ _lexer = new Lexer(ref);
+ references._frefs = new Ary<>(FRefNode.class);
+ _requiredTypes.clear();
+ // Starting Scope has control, memory, initial arguments
+ _scope = new ScopeNode();
+ _scope.define(ScopeNode.CTRL, Type.CONTROL , false, null, _lexer);
+ _scope.define(ScopeNode.MEM0, TypeMem.BOT , false, null, _lexer);
+ // Track active scopes for Graph display
+ _xScopes.push(_scope);
+ ctrl(_code.XCTRL);
+ mem(con(TypeMem.BOT));
+
+ // File-level struct declaration
+ _ref = ref;
+ _nestedType = ref._cname;
+ String typeName = TypeStruct.addClzPrefix(ref._cname);
+
+ // Parse whole file, which is a struct-level class initializer
+ structs.parseInitializer( true, typeName );
+
+ if( !_lexer.isEOF() ) throw _errorSyntax("unexpected");
+
+ Ary<FRefNode> frefs = references.resolve();
+
+ // The class being published
+ ref._clz = (TypeStruct)TYPES.get(typeName);
+
+ // Clean up and reset
+ _xScopes.pop();
+ _scope.kill();
+ _scope = null;
+ references._frefs = null;
+
+ return frefs;
+ }
+
+ // Cursor and token helpers
+
+ // Return true and skip if "syntax" is next in the stream.
+ boolean match (String syntax) { return _lexer.match (syntax); }
+ // Match must be "exact", not be followed by more id letters
+ boolean matchx(String syntax) { return _lexer.matchx(syntax); }
+ boolean matchOp(char c0 ) { return _lexer.matchOp(c0); }
+ boolean matchOpx(char c0, char c1) { return _lexer.matchOpx(c0,c1); }
+ // Skip whitespace, but leave the next token unread.
+ boolean peek(char ch) { return _lexer.peek(ch); }
+
+ public int pos() { return _lexer._position; }
+ int pos(int pos) {
+ int old = _lexer._position;
+ _lexer._position = pos;
+ return old;
+ }
+ Type posT(int pos) { _lexer._position = pos; return null; }
+ // Source code location
+ Lexer loc() { return new Lexer(_lexer); }
+
+ // Require and return an identifier
+ String requireId() {
+ String id = _lexer.matchId();
+ if (id != null && !KEYWORDS.contains(id) ) return id.intern();
+ throw error("Expected an identifier, found '"+id+"'");
+ }
+
+ String matchId() {
+ int old = pos();
+ String id = _lexer.matchId();
+ if( id==null ) return null;
+ if( !KEYWORDS.contains(id) ) return id;
+ pos(old);
+ return null;
+ }
+
+ // Require an exact match
+ Parser require(String syntax) { require(null, syntax); return this; }
+ <N> N require(N n, String syntax) {
+ if (match(syntax)) return n;
+ throw errorSyntax(syntax);
+ }
+
+ // Scope, control, and memory
+
+ // Read and set CTRL
+ Node ctrl() { return _scope.ctrl(); }
+ <N extends Node> N ctrl(N n) { return _scope.ctrl(n); }
+ // Read and set Memory
+ Node mem() { return _scope.mem(1); }
+ void mem(Node mem) { _scope.mem(mem); }
+
+ // Ordinary parsing carries one whole-memory value. Alias partitioning is
+ // graph optimization, not parser state.
+ void storeMem( Node st, int alias, Node prior ) {
+ mem(alias==1 ? st : mergeAlias(prior,alias,st));
+ }
+
+ void classInit(Lexer site, Node ptr) {
+ // Ordinary load machinery resolves the flag alias and offset when
+ // the receiver is still a forward reference.
+ Field flag = ptr._type instanceof TypeMemPtr tmp ? tmp._obj.field("$init") : null;
+ int alias = flag==null ? 1 : flag._alias;
+ Node ready = new LoadNode(site,"$init",alias,null,mem(),ptr,fldoff(ptr,"$init")).peephole();
+ CLInitNode init = new CLInitNode(site,ctrl(),mem(),ptr,ready).peephole().keep();
+ ctrl(new CProjNode(init,0,ScopeNode.CTRL).peephole());
+ Node bulk = new ProjNode(init,1,ScopeNode.MEM0).peephole().keep();
+ flag = init.flag();
+ mem(flag==null ? bulk : mergeAlias(bulk,flag._alias,
+ new ProjNode(init,2,ScopeNode.memName(flag._alias)).peephole()));
+ bulk.unkeep();
+ init.unkeep();
+ }
+
+ // IR construction using the current parser context
+
+ ConstantNode con( long con ) { return _code.con(con); }
+ ConstantNode con( Type t ) { return _code.con(t ); }
+
+ // A semantic graph partition representing the complete memory after a
+ // precise alias update. ScopeNode treats this as one opaque value.
+ Node mergeAlias(Node bulk, int alias, Node precise) {
+ assert bulk != null : "Missing bulk memory for alias #"+alias;
+ MemMergeNode merge = new MemMergeNode(null,bulk);
+ merge.alias(alias,precise);
+ return peep(merge);
+ }
+ // Field offset for an unknown type
+ public Node fldoff( Node n, String name ) {
+ if( n._type instanceof TypeMemPtr ptr && ptr._obj._fref )
+ _requiredTypes.add(ptr._obj._name);
+ return new FldOffNode(n,name,loc()).peephole();
+ }
+ public Node peep( Node n ) {
+ // Peephole, then improve with lexically scoped guards
+ return _scope.upcastGuard(n.peephole());
+ }
+
+ // Diagnostics
+
+ ParseException errorSyntax(String syntax) { return _errorSyntax("expected `"+syntax+"`"); }
+ private ParseException _errorSyntax(String msg) {
+ return error("Syntax error, "+msg+" but found `" + _lexer.getAnyNextToken()+"`");
+ }
+ ParseException error(String msg) { return error(msg,_lexer); }
+ public static ParseException error(String msg, Lexer loc) { return new ParseException(msg,loc); }
+
+ // Debugging
+
+ @Override
+ public String toString() { return _lexer==null ? "" : _lexer.toString(); }
+
+ // Debugging utility to find a Node by index
+ public Node f(int nid) { return _code.f(nid); }
+}
diff --git a/src/main/java/com/seaofnodes/simple/parser/README.md b/src/main/java/com/seaofnodes/simple/parser/README.md
new file mode 100644
index 00000000..ac0d9f29
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/README.md
@@ -0,0 +1,36 @@
+# Parser layout
+
+The parser builds Sea-of-Nodes IR directly. Each component shares the cursor,
+active scope, and compilation state through its `Parser P` reference; there is no intermediate
+AST. The [grammar map](../../../../../../../docs/chapter29/29-grammar.md) connects language
+productions to their entry points.
+
+| File | Responsibility |
+| --- | --- |
+| `Parser.java` | Source-unit entry point, shared state, type registry, and common token/IR helpers. |
+| `Lexer.java` | Tokens, literals, comments, source-location snapshots, and scanning deferred bodies. |
+| `Statements.java` | Statement dispatch, blocks, loops, and jumps. |
+| `Expressions.java` | Assignment and expression precedence, unary operators, primaries, and postfix dispatch. |
+| `Declarations.java` | Bindings, initializers, mutability modifiers, and native declarations. |
+| `Types.java` | Type syntax, qualified-name lookup, arrays, function types, and forward types. |
+| `Functions.java` | Function expressions, parameters, arguments, calls, and function entry/return graphs. |
+| `Structs.java` | Struct and constructor declarations, field discovery, and initialization Stores. |
+| `Construction.java` | Selects private self memory during parsing; its comment explains BOT fields, the TypeMemPtr qualifier, and checks deferred to TypeCheck. |
+| `References.java` | Bare-name and field resolution, implicit receivers, assignments, and forward references. |
+| `Allocations.java` | Allocation expressions, strings, and declaration-side allocation helpers. |
+| `ControlFlow.java` | Guarded scope merging shared by `if`, `?:`, `&&`, and `||`. |
+| `Conversions.java` | Shared binding, field, and result conversions. |
+| `Var.java` | Lexical binding metadata: name, type, scope slot, and mutability. |
+
+Start with `Statements.parseStatement` or `Expressions.parseAssignment` to follow
+the grammar. Loop updates are parsed after their bodies, and declared constructor
+bodies after field discovery; `Lexer.skipTo` scans these deferred fragments.
+
+`Parser` keeps the short cursor, scope/memory, IR-building, and diagnostic helpers
+used by these components. Independent helpers live with their data: class-object
+names in `TypeStruct`, postfix operator encoding in `Lexer`, known field-offset
+creation in `ConFldOffNode.make`, and memory-alias labels in `ScopeNode.memName`.
+
+Instance constructor fields live in ordinary memory, with one hidden `#selfMem`
+binding per activation. Forward field types use `TypeScalar.BOT` for missing
+initialization; there are no per-field snapshots or store-all constructor exits.
diff --git a/src/main/java/com/seaofnodes/simple/parser/References.java b/src/main/java/com/seaofnodes/simple/parser/References.java
new file mode 100644
index 00000000..483bd7a8
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/References.java
@@ -0,0 +1,315 @@
+package com.seaofnodes.simple.parser;
+
+import com.seaofnodes.simple.codegen.CompUnit;
+import com.seaofnodes.simple.codegen.ParseAll;
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.node.ScopeNode.Kind;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+
+/** Name and field access, receiver selection, and lvalue checks. */
+final class References {
+ private final Parser P;
+ // Forward references created during this unit.
+ Ary<FRefNode> _frefs;
+
+ References(Parser P) { this.P = P; }
+
+ // Resolve a primary name, retaining its use site before speculative type lookup.
+ Node parseName(String id, int pre, int pos, int pos2) {
+ // Attempt a local var lookup first
+ Var var = P._scope.lookup(id);
+ if( var==null ) {
+ // Not a local var, try again as a type name
+ P.pos(pos);
+ Type t = P.types.parseType();
+ if( t!=null ) {
+ // Static <clinit> typename lookup
+ if( P.peek('.') ) // type.fld, which is really a field lookup in the class object
+ return P.expressions.parsePostfix(P.con(t));
+ // Direct reference to a <clinit> type. Convert the instance
+ // type to a class type name
+ if( !(t instanceof TypeMemPtr tmp) ) {
+ //return new FRefNode(addClzPrefix(tmp._obj._name), loc()).peephole();
+ // Not a local var, but yes a bare type... disallow a local
+ // FREF and return null, not-a-primary parse.
+ P.pos(pos);
+ return null;
+ }
+ }
+
+ // Insert FREF outside enclosing Kind.Func scope.
+ P.pos(pos2);
+ t = FRefNode.FREF_TYPE;
+ // Check if this is a class.
+ CompUnit cu = ParseAll.findCompUnit(P._code,P._ref,id);
+ if( cu!=null ) {
+ // Must be a final class-type
+ String clzName = TypeStruct.addClzPrefix(cu._cname);
+ TypeStruct clz = (TypeStruct)Parser.TYPES.get(clzName);
+ if( clz == null )
+ Parser.TYPES.put(clzName,clz = TypeStruct.make(clzName,true));
+ t = TypeMemPtr.make((byte)2,clz,true);
+ }
+ // Define the FRef
+ var = P._scope.defineFRef(id,t,true,P.loc());
+ P.references._frefs.push((FRefNode)P._scope.in(var));
+ }
+
+ // Load local value
+ Node rvalue = P._scope.in(var);
+ if( var._extern!=0 )
+ return parseField(rvalue,id,pre);
+ if( var._fref )
+ return P.expressions.parsePostfix(rvalue);
+ Node fieldSelf = fieldSelf(var);
+ if( fieldSelf!=null ) return parseField(fieldSelf,id,pre);
+ int kx = P._scope.kindx(var);
+ int fx = P._scope.enclosingFunction();
+
+ // Check for a function-escaping variable; these require true
+ // closures. Final constants are OK; final vars require a hidden var
+ // argument; not-final fields can just use an explicit struct arg.
+ if( kx < fx && !P.structs.inExplicitConstructor() && // Out of scope
+ !(var._final && rvalue._type.isConstant()) ) { // Not a final constant
+ if( kx > 0 ) // Global scope is OK, only ever one of these (not one per function invoke)
+ throw P.error("Variable '"+var._name+"' is out of function scope and must be a final constant");
+
+ // var.idx==2 is self argument, 3 is args, 4 or more are now static globals
+ if( var._idx >= 4 ) {
+ // Read from global scope.
+ // Surely a nicer way to get the top-level scope compile name?
+ Var clzptr = P._scope.var( 2 );
+ // Get the most recent sharpen global
+ TypeStruct clz = (TypeStruct) Parser.TYPES.get( ((TypeMemPtr) clzptr.type())._obj._name );
+ TypeMemPtr clztmp = TypeMemPtr.make( (byte) 2, clz, true );
+ return parseField( P.con( clztmp ), id,pre );
+ //return parseField(_scope.in(2),id);
+ }
+ }
+
+ // Check for assign-update, x += e0;
+ char ch = pre==0 ? P._lexer.matchOperAssign() : (char)pre;
+ if( ch==0 ) { // Normal primary, check for postfix updates
+ int selfx = P._scope.kindFcn(var)._lexSize;
+ // Might be a method call from inside a method, so no explicit "self".
+ // Pass the in-scope "self"
+ return P.expressions.parsePostfixMethod(P.construction.view(rvalue),P._scope.in(P._scope.update(P._scope.var(selfx),null)));
+ }
+ // Assign-update direct into Scope
+ if( var._final && !P._scope.canInit(var) )
+ throw P.error("Cannot reassign final '"+var._name+"'");
+ checkSharedFunction(var);
+ Node op = P.expressions.opAssign(ch,rvalue, var.type() );
+ P._scope.update(var,op);
+ if( Lexer.postfix(ch) ) rvalue.unkeep();
+ return pre==0 && Lexer.postfix(ch) ? rvalue : op;
+ }
+
+ void checkAssignment(Var def) {
+ String name = def._name;
+ // A final field may receive its first value in its constructor.
+ if( P._scope.in(def._idx)._type!=Type.TOP && def._final &&
+ // Inside an allocation, final assign is OK, outside nope.
+ // The parseNew() call added the allocation scope
+ !P._scope.canInit(def) )
+ throw P.error("Cannot reassign final '"+name+"'");
+ checkSharedFunction(def);
+
+ }
+
+ Node assign(Var def, Node expr) {
+ Node self = fieldSelf(def);
+ if( self!=null ) {
+ TypeStruct ts = P.types.latestStruct(((TypeMemPtr)self._type)._obj);
+ Field fld = ts.field(def._name);
+ Node val = expr.keep();
+ Node lift = P.conversions.convertExpr(val,def.type());
+ Node mem = P.construction.storeMemory(self).keep();
+ Node st = new StoreNode(P.loc(),fld._fname,fld._alias,fld._t,P.ctrl(),mem,self,
+ ConFldOffNode.make(ts,fld._fname),lift,P.construction.own(self)).peephole();
+ P.construction.store(self,fld._alias,st,mem);
+ mem.unkeep();
+ return val.unkeep();
+ }
+ String name = def._name;
+ // Lift expression, based on type
+ Type decl = def.type(); // Declared type
+ Node lift = P.conversions.convertExpr(expr.keep(), decl).keep();
+ // Update
+ P._scope.update(name,lift);
+
+ lift.unkill();
+
+ // Return un-lifted expr
+ return expr.unkeep();
+ }
+
+ /**
+ * Parse postfix expression; this can be a field expression, an array
+ * lookup or a postfix operator like '#'
+ *
+ * <pre>
+ * expr ['++' | '--' ] // Postfix unary write operator
+ * expr [ [op]= expr ] // Field reference
+ * expr ('[' expr ']')* [ [op]= expr ] // Array reference
+ * </pre>
+ */
+
+ // pre is +/-1 for a prefix update, otherwise parse a postfix operation.
+ Node parseField(Node expr, String name, int pre) {
+ return parseField(expr,name,pre,P.loc());
+ }
+
+ Node parseField(Node expr, String name, int pre, Lexer site) {
+ // Keep expr across possible updates
+ expr.keep();
+
+ // Get the best known TypeStruct from a pointer type.
+ TypeStruct ts = null, ts2;
+ if( expr._type instanceof TypeMemPtr tmp && (ts2 = ((TypeStruct)Parser.TYPES.get(tmp._obj._name))) != null )
+ ts = (TypeStruct)tmp._obj.join(ts2);
+ // A Load's flow type is intentionally not promised during parsing,
+ // but its declaring field type is still known. Recover that static
+ // type for chained accesses such as `_grow(1).buf[len++]`; otherwise
+ // the array Store is parsed as alias #1 and can hide the independent
+ // side effect of the postfix `len++`.
+ if( ts==null && expr instanceof LoadNode load ) {
+ TypeStruct ots = load.off() instanceof ConFldOffNode coff ? coff._ts : null;
+ if( ots==null && load.ptr()._type instanceof TypeMemPtr owner ) {
+ Type owner0 = Parser.TYPES.get(owner._obj._name);
+ ots = owner0 instanceof TypeStruct obj ? obj : owner._obj;
+ }
+ if( ots != null ) {
+ Field loaded = ots.field(load._name);
+ if( loaded != null && loaded._t instanceof TypeMemPtr ltmp ) {
+ Type loaded0 = Parser.TYPES.get(ltmp._obj._name);
+ ts = loaded0 instanceof TypeStruct obj ? obj : ltmp._obj;
+ }
+ }
+ }
+
+ Field fld = ts==null ? null : ts.field(name);
+ if( fld==null && ts!=null && !TypeStruct.startsClzPrefix(ts._name) ) {
+ Type clz0 = Parser.TYPES.get(TypeStruct.addClzPrefix(ts._name));
+ if( clz0 instanceof TypeStruct clz && (fld = P.types.latestStruct(clz).field(name)) != null ) {
+ expr.unkill();
+ ts = P.types.latestStruct(clz);
+ expr = P.con(TypeMemPtr.make((byte)2,ts,true)).keep();
+ }
+ }
+ if( fld==null && ts!=null )
+ fld = P.types.fieldOrOpen(ts,name,false);
+ if( fld==null && ts!=null && !P.types.latestStruct(ts)._open )
+ throw P.error("Accessing unknown field '"+name+"' from '*"+ts._name+"'");
+
+ // We have to record "active touches" of a new class, to track when we
+ // need to force class initialization. Basically, we insert an CLInit,
+ // and if it does not fold away it'll turn into a runtime check.
+ Kind.Func func = (Kind.Func)P._scope._kinds.at(P._scope.enclosingFuncOrDecl());
+ if( (ts==null || ts==TypeStruct.BOT || ts.isHigh() || TypeStruct.startsClzPrefix(ts._name)) &&
+ !(ts!=null && (ts._name+".<clinit>").equals(func._name)) )
+ P.classInit(site,expr);
+
+ // With no field, we will update the bulk memory
+ int alias = fld==null ? 1 : fld._alias;
+ Type decl = fld==null ? Type.BOTTOM : fld._t;
+
+ // Field offset; fixed for structs, computed for arrays
+ Node off = (name=="[]"
+ // Array element math, with unknown base offset and shift amount.
+ ? P.peep(new AddNode(P.fldoff(expr,"[]"),P.peep(new ShlNode(null,P.require(P.expressions.parseAssignment(),"]"),P.fldoff(expr,"<<")))))
+ // Named field offset on an unknown struct type
+ : P.fldoff(expr,name)).keep();
+
+ // Disambiguate "obj.fld==x" boolean test from "obj.fld=x" field assignment
+ if( pre==0 && P.matchOpx('=','=') ) {
+ // Field assignment
+ Node val = P.expressions.parseAssignment().keep();
+ // Lift value for store
+ Node lift = fld==null ? val : new ConvertNode(fld._t,val).peephole();
+ // Memory for store, post assignment expression
+ Node mem = P.construction.storeMemory(expr);
+ // Store to field
+ mem.keep();
+ if( !P.construction.own(expr) && expr._type instanceof TypeMemPtr view && view._ro && P.ctrl()._type!=Type.XCONTROL )
+ throw P.error("Cannot modify final field '"+name+"'");
+ Node st = new StoreNode(P.loc(), name, alias, decl, P.ctrl(), mem, expr.unkeep(), off.unkeep(), lift, P.construction.own(expr)).peephole().keep();
+ P.construction.store(expr,alias,st,mem);
+ st.unkeep();
+ mem.unkeep();
+
+ return val.unkeep(); // "obj.a = expr" returns the expression while updating memory
+ }
+
+ // Memory for load
+ Node mem = P.construction.memory(expr);
+ // Loading from a constant array, the declared type is the
+ // meet-over-elements and not the array itself.
+ if( name=="[]" && decl instanceof TypeConAry conary )
+ decl = conary.elem();
+
+ // Load field
+ Node load = P.peep(new LoadNode(P.loc(),name,alias,P.ctrl(),mem,expr,off));
+
+ // Check for assign-update, "ptr.fld += expr" or "ary[idx]++"
+ char ch = pre==0 ? P._lexer.matchOperAssign() : (char)pre;
+ if( ch!=0 ) {
+ if( fld!=null && decl==Type.BOTTOM )
+ throw P.error( "'" + ts._name + "' is not fully initialized, field '" + fld._fname + "' needs to be set in a constructor" );
+ Node op = P.expressions.opAssign(ch,load, decl ).keep();
+ mem.keep();
+ if( !P.construction.own(expr) && expr._type instanceof TypeMemPtr view && view._ro && P.ctrl()._type!=Type.XCONTROL )
+ throw P.error("Cannot modify final field '"+name+"'");
+ Node st = new StoreNode(P.loc(), name, alias, decl, P.ctrl(), mem, expr.unkeep(), off.unkeep(), op, P.construction.own(expr)).peephole().keep();
+ P.construction.store(expr,alias,st,mem);
+ st.unkeep();
+ mem.unkeep();
+
+ op.unkeep(); // A narrow Store may have folded away the expression's conversion.
+ if( Lexer.postfix(ch) ) load.unkeep();
+ return pre==0 && Lexer.postfix(ch) ? load : op;
+ }
+ off.unkill();
+ // Might be a method call, so pass expr as 'self'
+ return P.expressions.parsePostfixMethod(load,expr.unkeep());
+ }
+
+ Node defaultSelf() {
+ int selfx = P._scope._kinds.at(P._scope.enclosingFuncOrDecl())._lexSize;
+ return P._scope.in(P._scope.update(P._scope.var(selfx),null));
+ }
+
+ // Fields are lvalues in memory, including inside their initializer and
+ // explicit constructor. Scope slots retain declaration metadata only.
+ private Node fieldSelf(Var var) {
+ int kx = P._scope.kindx(var), fx = P._scope.enclosingFuncOrDecl();
+ ScopeNode.Kind kind = P._scope._kinds.at(kx);
+ if( !(kind instanceof Kind.Func inst) || !FunNode.isInstance(inst._name) || var._idx<inst._lexSize+2 ) return null;
+ if( kx==fx || kx+1==fx ) return defaultSelf();
+ return null;
+ }
+
+ private void checkSharedFunction(Var v) {
+ if( P._scope.canInit(v) && v._final && v.type() instanceof TypeFunPtr t && t.isConstant() )
+ throw P.error("Cannot reassign shared function '"+v._name+"'");
+ }
+
+ Ary<FRefNode> resolve() {
+ // All still-unresolved forward refs
+ // should immediately resolve to a file-based name which is a Simple
+ // class type.
+ Ary<FRefNode> frefs = new Ary<>(FRefNode.class);
+ for( FRefNode fref : _frefs ) {
+ if( fref.nIns() != 1 && fref._con == FRefNode.FREF_TYPE ) continue;
+ CompUnit cu = ParseAll.findCompUnitOrThrow(P._code,P._ref,fref._name);
+ // Name lookup may already know this class's fields. Do not replace
+ // that information with an empty placeholder before Iter runs.
+ if( fref._con==FRefNode.FREF_TYPE )
+ fref.liftType(TypeMemPtr.make((byte)2,TypeStruct.make(TypeStruct.addClzPrefix(cu._cname),true),true));
+ frefs.push(fref);
+ }
+
+ return frefs;
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Statements.java b/src/main/java/com/seaofnodes/simple/parser/Statements.java
new file mode 100644
index 00000000..4ff4993b
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/Statements.java
@@ -0,0 +1,272 @@
+package com.seaofnodes.simple.parser;
+
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.node.ScopeNode.Kind;
+import com.seaofnodes.simple.type.*;
+
+/** Blocks, statements, and loops; builds their control-flow scopes. */
+final class Statements {
+ private final Parser P;
+
+ // Merge all the while-breaks here
+ ScopeNode _breakScope;
+ // Merge all loop continues here.
+ ScopeNode _continueScope;
+
+ Statements(Parser P) { this.P = P; }
+
+ /**
+ * Parses a statement
+ *
+ * <pre>
+ * returnStatement | declStatement | blockStatement | ifStatement | expressionStatement
+ * </pre>
+ * @return a {@link Node} or {@code null}
+ */
+ Node parseStatement() {
+ if( P.matchx("return") ) return parseReturn();
+ else if (P.matchx("if") ) return parseIf();
+ else if (P.matchx("while") ) return parseWhile();
+ else if (P.matchx("for") ) return parseFor();
+ else if (P.matchx("break") ) return parseBreak();
+ else if (P.matchx("continue")) return parseContinue();
+ else if (P.matchx("struct") ) return P.structs.parseStruct();
+ else if (P.structs.inStructDeclaration() && P.matchx("new")) return P.structs.parseConstructorDeclaration();
+ else if (P.matchx(";") ) return P._code.ZERO; // Empty statement
+ // Break ambiguity around leading function types and starting a block
+ else if (P.peek('{') && !P.types.isTypeFun() ) {
+ P.match("{");
+ return P.require(parseBlock(new Kind.Block()),"}");
+ }
+ // Declaration or normal assignment/expression
+ else return P.declarations.parseDeclarationStatement();
+ }
+
+ /**
+ * Parses a block
+ *
+ * <pre>
+ * '{' statements '}'
+ * </pre>
+ * Does not parse the opening or closing '{}'
+ * @return a {@link Node} or {@code null}
+ */
+ private Node parseBlock(Kind kind) {
+ // Enter a new scope
+ P._scope.push(kind);
+ Node last = P._code.ZERO;
+ while (!P.peek('}') && !P._lexer.isEOF()) {
+ P._code.add(last);
+ last = parseStatement();
+ }
+ // Exit scope
+ last.keep();
+ P._scope.pop();
+ return last.unkeep();
+ }
+
+ /**
+ * Parses a return statement; "return" already parsed.
+ * The $ctrl edge is killed.
+ *
+ * <pre>
+ * 'return' expr ;
+ * </pre>
+ * @return an expression {@link Node}, never {@code null}
+ */
+ private Node parseReturn() {
+ var expr = P.require(P.expressions.parseAssignment(), ";");
+ return P.functions.addReturn(expr);
+ }
+
+ /**
+ * Parses a statement
+ *
+ * <pre>
+ * if ( expression ) statement [else statement]
+ * </pre>
+ * @return a {@link Node}, never {@code null}
+ */
+ private Node parseIf() {
+ // Parse predicate
+ P.require("(");
+ var pred = P.require(P.expressions.parseAssignment(), ")");
+ return P.controlflow.conditional(pred,"else");
+ }
+
+ /**
+ * Parses a while statement
+ *
+ * <pre>
+ * while ( expression ) statement
+ * </pre>
+ * @return a {@link Node}, never {@code null}
+ */
+ private Node parseWhile() {
+ P.require("(");
+ return parseLooping(false);
+ }
+
+ /**
+ * Parses a for statement
+ *
+ * <pre>
+ * for( var x=init; test; incr ) body
+ * </pre>
+ * @return a {@link Node}, never {@code null}
+ */
+ private Node parseFor() {
+ // { var x=init,y=init,...;
+ // while( test ) {
+ // body;
+ // next;
+ // }
+ // }
+ P.require("(");
+ P._scope.push(new Kind.Block()); // Scope for the index variables
+ if( !P.match(";") ) // Can be empty init "for(;test;next) body"
+ P.declarations.parseDeclarationStatement(); // Non-empty init
+ Node rez = parseLooping(true);
+ P._scope.pop(); // Exit index variable scope
+ return rez;
+ }
+
+ // Shared by `for` and `while`
+ private Node parseLooping( boolean doFor ) {
+
+ var savedContinueScope = _continueScope;
+ var savedBreakScope = _breakScope;
+
+ // Loop region has two control inputs, the first is the entry
+ // point, and second is back edge that is set after loop is parsed
+ // (see end_loop() call below). Note that the absence of back edge is
+ // used as an indicator to switch off peepholes of the region and
+ // associated phis; see {@code inProgress()}.
+
+ P.ctrl(new LoopNode(P.loc(),null,P.ctrl()).peephole()); // Note we set back edge to null here
+
+ // At loop head, we clone the current Scope (this includes all
+ // names in every nesting level within the Scope).
+ // We create phis eagerly for all the names we find, see dup().
+
+ // Save the current scope as the loop head
+ ScopeNode head = P._scope.keep();
+ // Clone the head Scope to create a new Scope for the body.
+ // Create phis eagerly as part of cloning
+ P._xScopes.push(P._scope = P._scope.dup(true)); // The true argument triggers creating phis
+
+ // Parse predicate
+ var pred = P.peek(';') ? P.con(1) : P.expressions.parseAssignment();
+ P.require( doFor ? ";" : ")" );
+
+ // IfNode takes current control and predicate
+ Node ifNode = new IfNode(P.ctrl(), pred.keep()).peephole();
+ // Setup projection nodes
+ CFGNode ifT = new CProjNode(ifNode. keep(), 0, "True" ).peephole().keep();
+ CFGNode ifF = (CFGNode)new CProjNode(ifNode.unkeep(), 1, "False").peephole();
+
+ // for( ;;next ) body
+ int nextPos = -1, nextEnd = -1;
+ if( doFor ) {
+ // Skip the next expression and parse it later
+ nextPos = P.pos();
+ P._lexer.skipTo('(',')',"for update");
+ nextEnd = P.pos();
+ P.require(")");
+ }
+
+ // Clone the body Scope to create the break/exit Scope which accounts for any
+ // side effects in the predicate. The break/exit Scope will be the final
+ // scope after the loop, and its control input is the False branch of
+ // the loop predicate. Note that body Scope is still our current scope.
+ P.ctrl(ifF);
+ P._xScopes.push(_breakScope = P._scope.dup());
+ _breakScope.addGuards(ifF,pred,true); // Up-cast predicate
+
+ // No continues yet
+ _continueScope = null;
+
+ // Parse the true side, which corresponds to loop body
+ // Our current scope is the body Scope
+ P.ctrl(ifT.unkeep()); // set ctrl token to ifTrue projection
+ P._scope.addGuards(ifT,pred.unkeep(),false); // Up-cast predicate
+ parseStatement().isKill(); // Parse loop body
+ P._scope.removeGuards(ifT);
+
+ // Merge the loop bottom into other continue statements
+ if (_continueScope != null) {
+ _continueScope = jumpTo(_continueScope);
+ P._scope.kill();
+ P._scope = _continueScope;
+ }
+
+ // Now append the next code onto the body code
+ if( doFor ) {
+ int old = P.pos(nextPos);
+ if( !P.peek(')') )
+ P.expressions.parseAssignment();
+ if( P.pos() != nextEnd )
+ throw P.errorSyntax( "Unexpected code after expression" );
+ P.pos(old);
+ }
+
+ // The true branch loops back, so whatever is current _scope.ctrl gets
+ // added to head loop as input. endLoop() updates the head scope, and
+ // goes through all the phis that were created earlier. For each phi,
+ // it sets the second input to the corresponding input from the back
+ // edge. If the phi is redundant, it is replaced by its sole input.
+ var exit = _breakScope;
+ head.balanceLoopFRefs(P._scope);
+ exit.balanceLoopFRefs(P._scope);
+ head.endLoop(P._scope, exit);
+ head.unkeep().kill();
+
+ P._xScopes.pop(); // Cleanup
+ P._xScopes.pop(); // Cleanup
+
+ _continueScope = savedContinueScope;
+ _breakScope = savedBreakScope;
+
+ // At exit the false control is the current control, and
+ // the scope is the exit scope after the exit test.
+ // During sys parsing, there is no xscope here.
+ if( !P._xScopes.isEmpty() ) {
+ P._xScopes.pop();
+ P._xScopes.push( exit );
+ }
+ P._scope = exit;
+ return P._code.ZERO;
+ }
+ private Node parseBreak () {
+ checkLoopActive();
+ // At the time of the break, and loop-exit conditions are only valid if
+ // they are ALSO valid at the break. It is the intersection of
+ // conditions here, not the union.
+ _breakScope.removeGuards(_breakScope.ctrl());
+ _breakScope = P.require(jumpTo(_breakScope ),";");
+ _breakScope.addGuards((CFGNode)_breakScope.ctrl(), null, false);
+ return P._code.ZERO;
+ }
+
+ private Node parseContinue() { checkLoopActive(); _continueScope = P.require(jumpTo( _continueScope ),";"); return P._code.ZERO; }
+
+ private ScopeNode jumpTo(ScopeNode toScope) {
+ ScopeNode cur = P._scope.dup();
+ P.ctrl(P._code.XCTRL); // Kill current scope
+ // Prune nested lexical scopes that have depth > than the loop head
+ // We use _breakScope as a proxy for the loop head scope to obtain the depth
+ while( cur.depth() > _breakScope.depth() )
+ cur.pop();
+ // If this is a continue then first time the target is null
+ // So we just use the pruned current scope as the base for the
+ // "continue"
+ if( toScope == null )
+ return cur;
+ // toScope is either the break scope, or a scope that was created here
+ assert toScope.depth() <= _breakScope.depth();
+ toScope.ctrl(toScope.mergeScopes(cur,P.loc()).peephole());
+ return toScope;
+ }
+
+ private void checkLoopActive() { if (_breakScope == null) throw P.error("No active loop for a break or continue"); }
+}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Structs.java b/src/main/java/com/seaofnodes/simple/parser/Structs.java
new file mode 100644
index 00000000..a5262440
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/Structs.java
@@ -0,0 +1,338 @@
+package com.seaofnodes.simple.parser;
+
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.node.ScopeNode.Kind;
+import com.seaofnodes.simple.type.*;
+
+/** Struct and constructor declarations, field discovery, and initialization checks. */
+final class Structs {
+ private final Parser P;
+
+ // Constructor declaration: new TypeName = { args -> body };
+ record Constructor(String name, Lexer body) {}
+ Constructor _pendingCtor;
+
+ Structs(Parser P) { this.P = P; }
+
+ /**
+ * Parse a struct declaration, and return the following statement.
+ * Structs cannot be redefined, but can be nested.
+ *
+ * @return zero
+ */
+ Node parseStruct( ) {
+ // keyword "struct" already parsed, so expect the struct name next.
+ String typeName = P.requireId();
+ P.require("{");
+
+ // create type class:NESTED.typeName; no fields.
+
+ String old = P._nestedType;
+ String fullName = P._scope.depth()==1 && P._nestedType.endsWith(typeName) ? old : (old+"."+typeName).intern();
+ P._nestedType = fullName; // Recursive structs start with this as their basename
+
+ ReturnNode ret = parseInitializer( false, fullName );
+
+ P._nestedType = old; // Pop nested stack
+
+ // Insert a field into class:NESTED.typeName of "val typeName = fcn-ptr-to-init"
+ // Function does malloc internally.
+ // Future self: means subtyping has to deal with "who does the malloc"
+ String clzName = TypeStruct.addClzPrefix(fullName);
+ TypeFunPtr sig = ret.fun().sig();
+ TypeStruct ts = (TypeStruct)Parser.TYPES.get(clzName);
+ String initName = initFieldName(fullName);
+ Field oldFld = ts==null ? null : ts.field(initName);
+ Field fld = oldFld==null
+ ? Field.make(initName,sig,P._code.alias(P._ref._cname),true)
+ : oldFld.makeFrom(sig);
+ Field ctorFld = ts==null ? null : ts.field("<ctor>");
+ // A same-named struct shares its file's class object and flag.
+ Field flag = ts==null ? null : ts.field("$init");
+ ts = ctorFld==null
+ ? TypeStruct.make(clzName,false,fld)
+ : TypeStruct.make(clzName,false,fld,ctorFld);
+ if( flag!=null ) ts = ts.add(flag);
+ TypeFunPtr alloc = P.allocations.makeAllocator((TypeStruct)Parser.TYPES.get(fullName),ret,ctorFld);
+ // The visible constructor name denotes allocation, not the hidden initializer.
+ sig = alloc;
+ ts = ts.replace(fld.makeFrom(alloc));
+ ts = ts.add(Field.make("<new>",alloc,P._code.alias(P._ref._cname),true));
+ Parser.TYPES.put(clzName,ts);
+
+ // Insert a field in the containing class with the nested class type.
+ // This is basically sugar for "val CLZNAME = <new>"
+ // and is a nicer version of updateSelfAsFieldsDiscovered.
+ P._scope.define(typeName, sig, true, new FunPtrNode(sig,P._code._start,P._code.link(alloc).ret()).peephole(), P.loc());
+ Var binding = P._scope.lookup(typeName);
+ declareField(binding,P._scope.in(binding),false);
+
+ P.require("}");
+ return P.require(P._code.ZERO,";");
+ }
+
+ boolean inStructDeclaration() {
+ return P._scope.klast() instanceof Kind.Func fun && FunNode.isInstance(fun._name);
+ }
+
+ Node parseConstructorDeclaration() {
+ TypeStruct self = P._scope.constructorSelf();
+ assert self != null;
+ String name = P.requireId();
+ String expect = initFieldName(self._name);
+ if( name != expect )
+ throw P.error("Constructor name '" + name + "' must match type name '" + expect + "'");
+ P.require("=");
+ if( _pendingCtor!=null ) throw P.error("Redefining constructor '"+name+"'");
+ _pendingCtor = new Constructor(name,P.loc());
+ // Parse the body after all field declarations are known, so self.x
+ // and x resolve the same field even when x is declared below this body.
+ P.require("{");
+ P._lexer.skipTo('{','}',"constructor body");
+ P.require("}");
+ P.require(";");
+ return P._code.ZERO;
+ }
+
+ void finishConstructor() {
+ if( _pendingCtor==null ) return;
+ Constructor ctor = _pendingCtor;
+ _pendingCtor = null;
+ Lexer after = P._lexer;
+ P._lexer = new Lexer(ctor.body());
+ defineConstructor(ctor.name());
+ P._lexer = after;
+ }
+
+ private void defineConstructor(String name) {
+ TypeStruct self = P._scope.constructorSelf();
+ Node ctor = P.functions.parseConstructor(name,true);
+ TypeFunPtr sig = (TypeFunPtr)ctor._type;
+ String clzName = TypeStruct.addClzPrefix(self._name);
+ TypeStruct clz = (TypeStruct)Parser.TYPES.get(clzName);
+ Field old = clz==null ? null : clz.field("<ctor>");
+ if( old != null )
+ throw P.error("Redefining constructor '" + name + "'");
+ Field fld = Field.make("<ctor>",sig,P._code.alias(P._ref._cname),true);
+ Parser.TYPES.put(clzName,clz==null ? TypeStruct.make(clzName,true,fld) : clz.add(fld));
+ P.require(ctor,";");
+ }
+
+ // Parse a struct declaration (not an allocation); file-level is a
+ // class-init, and scope structs are normal
+ ReturnNode parseInitializer( boolean isClz, String typeName ) {
+ // Record & restore global state set during parsing the <init> code
+ Node oldCtrl= P._scope.ctrl().keep();
+ Node oldMem = P._scope.mem ().keep();
+
+ // Make the future clazz/instance struct.
+ TypeStruct tself = (TypeStruct)Parser.TYPES.get(typeName);
+ assert tself==null || tself._open;
+ // Close an existing open struct, or define a forward ref
+ tself = tself==null ? TypeStruct.make(typeName,true) : tself.defined();
+
+ // Class object keeps a runtime flag `$init` to signal the <clinit> has run.
+ if( isClz )
+ tself = tself.add(Field.make("$init",TypeInteger.BOOL,P._code.alias(P._ref._cname),false));
+ Parser.TYPES.put(typeName, tself);
+
+ // Signature of the default constructor
+ TypeFunPtr sig = structInitSig(isClz,tself);
+ // Function and argument names of default constructor
+ String fname = (typeName + (isClz ? ".<clinit>" : ".<init>")).intern();
+ String[] ids = new String[]{"self", isClz ? "arg" : "#selfMem"};
+
+ // Struct decls look like function bodies. Parse function body normally.
+ ReturnNode ret = P.functions.parseBody(fname,sig,P.loc(),ids);
+ // Unwind global state.
+ P.ctrl(oldCtrl.unkeep());
+ P.mem (oldMem .unkeep());
+ return ret;
+ }
+
+ // <cl/init> signature: { self arg/selfMem -> BOT/selfMem }
+ // Self-memory is the rare *private* (never aliased) memory for the new object.
+ private TypeFunPtr structInitSig( boolean isClz, TypeStruct tself ) {
+ // One object, unknown contents. The signature must not promise that
+ // its declared field values have already been supplied.
+ Type targ = isClz ? TypeInteger.BOT : TypeMem.makePrivate(Type.BOTTOM);
+ // Return for private memory has to name all escaped fields, which are not known yet.
+ // Escape ALL fields, and sharpen later.
+ Type tret = isClz ? Type.BOTTOM : TypeMem.makePrivate(Type.BOTTOM);
+ Type[] targs = new Type[]{ TypeMemPtr.make((byte)2,tself,isClz).withPartial(!isClz), targ };
+ // FIDX for FREFs is in the global namespace, needs the correct clz name.
+ String clzBare = tself._name;
+ int order = 3; // 3 is always the <init> order, 2nd in the file just behind the <clinit>
+ if( isClz ) {
+ assert TypeStruct.startsClzPrefix( clzBare );
+ clzBare = clzBare.substring(TypeStruct.CLZ.length()).intern();
+ order = 2; // 2 is always the <clinit> order, first in file and skipping reserved #1
+ }
+ return TypeFunPtr.make1((byte)2, true, targs, tret, P._code.fidx(clzBare,order));
+ }
+
+ int updateSelfAsFieldsDiscovered( String typeName, int nvar ) {
+ int lex = P._scope.klast()._lexSize + 2/*Skip self and selfMem*/;
+ // Load self type in and out of the TYPES hashtable.
+ TypeStruct tself = (TypeStruct)Parser.TYPES.get(typeName);
+ // Start looking for newly declared vars at the lexical scope start,
+ // skipping two ("self" and "selfmem") and fields seen already.
+ while( lex + nvar < P._scope.nIns() ) {
+ Var v = P._scope.var(lex + nvar++);
+ // Field with sharper type
+ Field old = tself.field(v._name);
+ Field fld = v._extern!=0
+ ? Field.make(v._name,v.type(),v._extern,false,true)
+ : old == null
+ ? Field.make(v._name,v.type(),P._code.alias(P._ref._cname),v._final)
+ : old.makeFrom(v.type());
+ tself = tself.addOrUpdate(fld);
+ }
+ Parser.TYPES.put(typeName,tself);
+ return nvar;
+ }
+
+ void finishInitializer( String typeName, boolean isClz, ParmNode self ) {
+ int lex = P._scope.klast()._lexSize;
+ assert P._scope.var(lex)._name=="self";
+ TypeStruct tself = (TypeStruct)Parser.TYPES.get(typeName);
+ int base = lex+2; // Skip ctrl, mem
+ // Default <init>/<clinit> defines the struct and can sharpen fields.
+ // A named constructor only initializes the already-closed struct.
+ for( int i = 0; i<tself.nkids(); i++ ) {
+ Field tfld = tself._fields[i];
+ int vidx = fieldVarIdx(base,tfld._fname);
+ if( vidx == -1 )
+ continue;
+ Var v = P._scope.var(vidx);
+ // Forward-ref fields where placed in tself but will get promoted
+ // to an outer scope - and should not be in tself.
+ if( v._fref )
+ tself = tself.remove(i--);
+ else if( v.type() != tfld._t ) {
+ assert v.type().isa(tfld._t);
+ tself = tself.replace( tfld.makeFrom( v.type() ) );
+ }
+ }
+
+ // Close Struct type after parsing struct body.
+ Parser.TYPES.put( tself._name, tself = tself.close() );
+ // Improve the receiver type in scope.
+ // Class self-type is a singleton pointer. Instance <init> receives the
+ // broad allocation pointer, while private memory tracks initialized fields.
+ self._declaredType = self._type = isClz ? TypeMemPtr.make((byte)2,tself,true) : TypeMemPtr.make(constructorRecv(tself)).withPartial(true);
+ FunNode fun = self.fun();
+ fun.setSig(fun.sig().makeFrom(self._type,0));
+
+ // When can _returnScope be null here? A never-exit constructor will
+ // not have any returns, and thus no need to gather values and store
+ // them into the (never) constructed object
+ if( P.functions._returnScope==null )
+ return;
+
+ // Class singleton fields retain their store-at-exit path. Instance
+ // initializers already returned their current private memory.
+ if( !isClz ) return; // Instance fields already have their Stores.
+ Node mmm = P.functions._returnScope.mem(1);
+
+ // Store constructor results into fields
+ for( int i = 0; i<tself.nkids(); i++ ) {
+ Field fld = tself._fields[i];
+ int vidx = fieldVarIdx(base,fld._fname);
+ if( vidx == -1 )
+ continue;
+ Var v = P._scope.var(vidx);
+ if( v._fref || v._extern!=0 ) continue; // C storage has no Simple initializer
+ Node val = P.functions._returnScope.in(vidx);
+ // A numeric merge can still be untyped while parsing. Retain its
+ // declared conversion at the helper boundary, as assignments do.
+ if( fld._t instanceof TypeInteger || fld._t instanceof TypeFloat )
+ val = P.conversions.convertExpr(val,fld._t);
+ // Store value into extended struct
+ Node prior = mmm.keep();
+ Node st = P.peep(new StoreNode(null, fld._fname, fld._alias, fld._t, null, prior, self, ConFldOffNode.make(tself,fld._fname), val, fld._final));
+ mmm = P.mergeAlias(prior,fld._alias,st);
+ prior.unkeep();
+ }
+ mmm = mmm.peephole();
+
+ P.functions._returnScope.mem(mmm);
+ }
+
+ // Instance declarations establish field metadata and emit their default
+ // Stores immediately. Required fields emit no Store until assigned.
+ void declareField(Var var, Node value, boolean uninit) {
+ if( !(P._scope.klast() instanceof Kind.Func kind) || !FunNode.isInstance(kind._name) ) return;
+ TypeStruct self = P._scope.constructorSelf();
+ TypeStruct shape = (TypeStruct)Parser.TYPES.get(self._name);
+ Field old = shape.field(var._name);
+ Field fld = old==null ? Field.make(var._name,var.type(),P._code.alias(P._ref._cname),var._final) : old.makeFrom(var.type());
+ Parser.TYPES.put(shape._name,shape = shape.addOrUpdate(fld));
+ if( uninit || var._extern!=0 ) return;
+ Node ptr = P.references.defaultSelf();
+ Node mem = P.construction.selfMem().keep();
+ Node st = new StoreNode(var._loc,fld._fname,fld._alias,fld._t,P.ctrl(),mem,ptr,
+ ConFldOffNode.make(shape,fld._fname),value,true).peephole();
+ P.construction.store(ptr,fld._alias,st,mem);
+ mem.unkeep();
+ }
+
+ void beginClassInit(FunNode fun, Lexer loc) {
+ // Each initializer establishes its parent before setting its own
+ // run-once flag. Keep this in the body so every execution strategy
+ // shares the same ordering, including direct entry into a child.
+ if( P._ref._par!=null ) {
+ String pname = TypeStruct.addClzPrefix(P._ref._par._cname);
+ TypeStruct parent = (TypeStruct)Parser.TYPES.get(pname);
+ if( parent==null ) parent = TypeStruct.make(pname,true);
+ Node ptr = P.con(TypeMemPtr.make((byte)2,parent,true)).keep();
+ P.classInit(loc,ptr);
+ ptr.unkeep();
+ }
+ Node self = fun.parm(2);
+ Field flag = ((TypeMemPtr)self._type)._obj.field("$init");
+ Node prior = P.mem().keep();
+ Node st = P.peep(new StoreNode(loc,"$init",flag._alias,flag._t,P.ctrl(),prior,self,
+ P.fldoff(self,"$init"),P.con(1),false));
+ P.storeMem(st,flag._alias,prior);
+ prior.unkeep();
+ }
+
+ private int fieldVarIdx( int base, String name ) {
+ for( int i = base; i<P._scope.nIns(); i++ )
+ if( P._scope.var(i)._name == name )
+ return i;
+ return -1;
+ }
+
+ boolean isConstructorDecl(String name) {
+ TypeStruct self = P._scope.constructorSelf();
+ return self != null && name == initFieldName(self._name);
+ }
+
+ boolean inExplicitConstructor() {
+ Kind.Func func = (Kind.Func)P._scope._kinds.at(P._scope.enclosingFuncOrDecl());
+ return isExplicitConstructorName(func._name);
+ }
+
+ boolean isExplicitConstructorName(String funName) {
+ TypeStruct self = P._scope.constructorSelf();
+ return self != null && funName == (self._name+"."+initFieldName(self._name)).intern();
+ }
+
+ static TypeStruct constructorRecv(TypeStruct self) {
+ Field[] fs = new Field[self._fields.length];
+ for( int i = 0; i<fs.length; i++ ) {
+ Field fld = self._fields[i];
+ fs[i] = fld.makeFrom(fld._t.makeStorage());
+ }
+ return TypeStruct.make(self._name,self._open,fs);
+ }
+
+ private static String initFieldName( String typeName ) {
+ if( TypeStruct.startsClzPrefix(typeName) )
+ typeName = typeName.substring(TypeStruct.CLZ.length());
+ int idx = typeName.lastIndexOf('.');
+ return (idx == -1 ? typeName : typeName.substring(idx+1)).intern();
+ }
+}
diff --git a/src/main/java/com/seaofnodes/simple/parser/Types.java b/src/main/java/com/seaofnodes/simple/parser/Types.java
new file mode 100644
index 00000000..ffd3a0f4
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/Types.java
@@ -0,0 +1,245 @@
+package com.seaofnodes.simple.parser;
+
+import com.seaofnodes.simple.codegen.CompUnit;
+import com.seaofnodes.simple.codegen.ParseAll;
+import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.type.*;
+import com.seaofnodes.simple.util.Ary;
+
+/** Type syntax, qualified type lookup, and forward type declarations. */
+final class Types {
+ private final Parser P;
+
+ Types(Parser P) { this.P = P; }
+
+ // Parse a type or restore the cursor and return null. Unknown names need
+ // a following binding to distinguish a forward type from an expression.
+
+ // Structs have nested names, and can be discovered with a partial name
+ // match. Example, searching for a type name "C.D" in the namespace "A.B".
+ // This will match "A.B.C.D", then "A.C.D", then "C.D".
+
+ // Will also match external names like "sys.io" or "externModule.subtype.subtype"
+
+ Type parseType() {
+ if( P.peek('{') ) return parseFunctionType();
+
+ int old1 = P.pos();
+ boolean writable = P.match("!");
+ boolean readonly = !writable && P.match("~");
+ String tname = P._lexer.matchId();
+ if( tname==null ) return P.posT(old1);
+
+ Type t0 = Parser.TYPES.get(tname);
+ if( Parser.KEYWORDS.contains(tname) ) {
+ if( t0==null ) return P.posT(old1);
+ } else {
+ if( t0==Type.BOTTOM || t0==Type.TOP ) return t0; // var/val
+ tname = typeName(tname).intern();
+ t0 = Parser.TYPES.get(tname);
+ }
+ Type t1 = t0==null ? TypeStruct.forward(tname) : t0;
+
+ // Structs are always actually references
+ if( t1 instanceof TypeStruct ts1 )
+ t1 = TypeMemPtr.make(ts1);
+
+ // A qualifier before the name applies to the struct reference, not
+ // to an enclosing array. Arrays have their own [] / [~] qualifier.
+ if( t1 instanceof TypeMemPtr tmp ) {
+ if( readonly || !writable ) t1 = tmp.makeRO();
+ } else if( writable || readonly )
+ return P.posT(old1);
+
+ // Nest arrays and '?' as needed
+ Type t2 = t1;
+ while( true ) {
+ if( P.match("?") ) {
+ if( t2 instanceof TypeMemPtr tmp ) {
+ if( tmp.nullable() ) throw P.error("Type "+t2+" already allows null");
+ t2 = tmp.makeNullable();
+ } else
+ throw P.error("Type "+t0+" cannot be null");
+ } else if( P.match("[~]") ) {
+ t2 = TypeMemPtr.make(typeAry(t2,true)).makeRO();
+ } else if( P.match("[]") ) {
+ t2 = TypeMemPtr.make(typeAry(t2,false));
+ } else
+ break;
+ }
+
+ // Check no forward ref
+ if( t0 != null ) return t2;
+ // Check valid forward ref, after parsing all the type extra bits.
+ // Cannot check earlier, because cannot find required 'id' until after "[]?" syntax
+ int old2 = P.pos();
+ P.match("^"); // Constructor parameter qualifier; parsed by Functions.
+ if( !P.match("!") ) P.match("~");
+ String id = P._lexer.matchId();
+ if( !(P.peek(',') || P.peek(';') || P.peek('=') || P.match("->")) || id==null )
+ return P.posT(old1); // Reset lexer to reparse
+ P.pos(old2); // Reset lexer to reparse
+ // Yes a forward ref, so declare it
+ forwardRefType(tname,(TypeStruct)((TypeMemPtr)t1)._obj);
+ // Return the (array, final) type
+ return t2;
+ }
+
+ TypeMemPtr forwardRefType( String tname, TypeStruct tself ) {
+ Parser.TYPES.put(tname,tself);
+ String clzName = TypeStruct.addClzPrefix( tname );
+ // Only a struct declaration publishes an instance initializer. A
+ // forward type may name a file with just <clinit>, so do not invent
+ // a constructor field (or overwrite an already parsed class).
+ Parser.TYPES.putIfAbsent(clzName,TypeStruct.make(clzName,true));
+ return TypeMemPtr.make(tself);
+ }
+
+ TypeMemPtr forwardRefType( String tname ) {
+ tname = fullTypeName(tname);
+ return forwardRefType(tname,TypeStruct.forward(tname));
+ }
+
+ String fullTypeName( String tname ) {
+ String nest = P._nestedType, fullq;
+ while( true ) {
+ fullq = nest+"."+tname;
+ if( Parser.TYPES.get(fullq) != null )
+ return fullq.intern();
+ int idx = nest.lastIndexOf('.');
+ if( idx== -1 ) return fullq.intern();
+ nest = nest.substring(0,idx);
+ }
+ }
+
+ // Make an array type of t. Always record a mutable version,
+ // but return the requested version.
+ TypeStruct typeAry( Type t, boolean efinal ) {
+ // Non-null reference arrays are valid types, even though an ordinary
+ // zero-filled `new T[len]` cannot construct one. Allocation retains
+ // that check in parseNew(); trusted runtimes (notably argv construction)
+ // may fill the private array completely before publishing it.
+ String tname = ("[]"+t.str()).intern();
+ TypeStruct ta = (TypeStruct)Parser.TYPES.get(tname);
+ if( ta==null ) {
+ // Primitive arrays use the null compunit
+ String rcname = Parser.TYPES.get(t) instanceof TypeStruct ? P._ref._cname : ("_."+t).intern();
+ ta = TypeStruct.makeAry(tname,TypeInteger.U32,P._code.alias(rcname),t,P._code.alias(rcname), false );
+ Parser.TYPES.put(tname,ta);
+ }
+ // Already have the aliases, just efinal is wrong
+ return TypeStruct.makeAry(tname,TypeInteger.U32,ta._fields[0]._alias,t,ta._fields[1]._alias,efinal );
+ }
+
+ Type implicitSelfType() {
+ TypeStruct self = P._scope.constructorSelf();
+ // Ordinary instance methods accept the declared object shape. The
+ // constructor-only receiver starts at the low/default field values,
+ // and is built explicitly by parseConstructor().
+ return self == null ? TypePtr.PTR : TypeMemPtr.make(self);
+ }
+
+ // A function type is `{ type... -> type }` or `{ type }`.
+ private Type parseFunctionType() {
+ int old = P.pos(); // Record lexer position
+ P.match("{"); // Skip already-peeked '{'
+ Type t0 = parseType(); // Either return or first arg
+ if( t0==null ) return P.posT(old); // Not a function
+ Ary<Type> ts = new Ary<>(Type.class);
+ // Always reserve a receiver slot. Outside constructor scopes the
+ // broad signature also marks the hidden slot for the machine ABI.
+ ts.push(implicitSelfType());
+ if( P.match("}") ) // No-arg function { -> type }
+ return TypeFunPtr.make(P.match("?"),ts.asAry(),t0);
+ ts.push(t0); // First argument
+ while( true ) {
+ if( P.match("->") ) { // End of arguments, parse return
+ Type ret = parseType();
+ if( ret==null || !P.match("}") )
+ return P.posT(old); // Not a function
+ return TypeFunPtr.make(P.match("?"),ts.asAry(),ret);
+ }
+ Type t1 = parseType();
+ if( t1==null ) return P.posT(old); // Not a function
+ ts.push(t1);
+ }
+ }
+
+ // True if a TypeFunPtr, without advancing parser
+ boolean isTypeFun() {
+ int old = P.pos();
+ if( parseFunctionType()==null ) return false;
+ P.pos(old);
+ return true;
+ }
+
+ TypeStruct latestStruct( TypeStruct ts ) {
+ Type t = Parser.TYPES.get(ts._name);
+ return t instanceof TypeStruct ts2 ? ts2 : ts;
+ }
+
+ Field fieldOrOpen( TypeStruct ts, String name, boolean xfinal ) {
+ ts = latestStruct(ts);
+ Field fld = ts.field(name);
+ if( fld != null ) return fld;
+ if( !ts._open || !ts._fref ) return null;
+ fld = Field.make(name,Type.BOTTOM,P._code.alias(P._ref._cname),xfinal);
+ Parser.TYPES.put(ts._name,ts = ts.add(fld));
+ return fld;
+ }
+
+ // typeName: walk known local types first, then enclosing namespaces, then
+ // file modules. A missing suffix of a known type belongs to field parsing.
+ private String typeName(String name) {
+ if( Parser.TYPES.containsKey(name) ) return dottedName(name,false);
+ String root = P._nestedType;
+ int dot = root.indexOf('.');
+ if( dot!=-1 ) root = root.substring(0,dot);
+ if( name.equals(root) ) return rootedName(name);
+
+ for( String nest = P._nestedType; ; ) {
+ String full = nest+"."+name;
+ if( Parser.TYPES.containsKey(full) ) return dottedName(full,false);
+ int idx = nest.lastIndexOf('.');
+ if( idx==-1 ) break;
+ nest = nest.substring(0,idx);
+ }
+ CompUnit cu = ParseAll.findCompUnit(P._code,P._ref,name);
+ return dottedName(cu==null ? name : cu._cname,true);
+ }
+
+ private String dottedName(String name, boolean forward) {
+ while( true ) {
+ int old = P.pos();
+ if( !P.match(".") ) return name;
+ String part = P.matchId();
+ if( part==null || (!forward && !Parser.TYPES.containsKey(name+"."+part)) ) {
+ P.pos(old);
+ return name;
+ }
+ name += "."+part;
+ }
+ }
+
+ // A root-qualified name may introduce an as-yet-undeclared nested type
+ // inside the current lexical namespace. Once forward, keep its full tail.
+ private String rootedName(String name) {
+ boolean forward = false;
+ while( true ) {
+ int old = P.pos();
+ if( !P.match(".") ) return name;
+ String part = P.matchId();
+ if( part==null ) { P.pos(old); return name; }
+ String full = name+"."+part;
+ boolean known = Parser.TYPES.containsKey(full);
+ if( !known && !forward &&
+ !(P._nestedType.equals(name) || P._nestedType.startsWith(name+".")) ) {
+ P.pos(old);
+ return name;
+ }
+ name = full;
+ forward |= !known;
+ }
+ }
+
+}
diff --git a/src/main/java/com/seaofnodes/simple/Var.java b/src/main/java/com/seaofnodes/simple/parser/Var.java
index af371d21..ddbb91d8 100644
--- a/src/main/java/com/seaofnodes/simple/Var.java
+++ b/src/main/java/com/seaofnodes/simple/parser/Var.java
@@ -1,4 +1,4 @@
-package com.seaofnodes.simple;
+package com.seaofnodes.simple.parser;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.util.Utils;
@@ -16,16 +16,13 @@ public class Var {
private Type _type; // Declared type
public boolean _final; // Final field
public boolean _fref; // Forward ref
- // Parser-only definite-initialization marker. The current binding's
- // nullable type carries the path-sensitive state through SSA merges.
- public boolean _uninit;
public int _extern; // C storage alias; zero for ordinary variables
- public Parser.Lexer _loc; // Source location
+ public Lexer _loc; // Source location
- public Var(int idx, String name, Type type, boolean xfinal, Parser.Lexer loc ) {
+ public Var(int idx, String name, Type type, boolean xfinal, Lexer loc ) {
this(idx,name,type,xfinal,loc,false);
}
- public Var(int idx, String name, Type type, boolean xfinal, Parser.Lexer loc, boolean fref) {
+ public Var(int idx, String name, Type type, boolean xfinal, Lexer loc, boolean fref) {
_idx = idx;
_name = name;
_type = type;
@@ -40,7 +37,7 @@ public class Var {
// distinct from inferred variables
public boolean isFRef() { return _fref; }
- public void defFRef( Type type, boolean xfinal, Parser.Lexer loc ) {
+ public void defFRef( Type type, boolean xfinal, Lexer loc ) {
assert isFRef() && xfinal;
_type = type;
_final = true;
@@ -49,6 +46,6 @@ public class Var {
}
@Override public String toString() {
- return (_fref ? "FREF " : "")+(_uninit ? "UNINIT " : "")+_type.toString()+(_final ? " ": " !")+_name;
+ return (_fref ? "FREF " : "")+_type.toString()+(_final ? " ": " !")+_name;
}
}
diff --git a/src/main/java/com/seaofnodes/simple/parser/package-info.java b/src/main/java/com/seaofnodes/simple/parser/package-info.java
new file mode 100644
index 00000000..7212e629
--- /dev/null
+++ b/src/main/java/com/seaofnodes/simple/parser/package-info.java
@@ -0,0 +1,14 @@
+/**
+ * Handwritten recursive-descent parsing directly into Sea-of-Nodes IR.
+ *
+ * <p>{@link com.seaofnodes.simple.parser.Parser} owns the shared cursor and
+ * scope. Package-local components implement the grammar families: statements,
+ * expressions, declarations, types, functions, and structs. References handles
+ * use-side name/field resolution; Allocations, ControlFlow, and Conversions
+ * contain their shared IR construction. Lexer also supplies location snapshots.
+ *
+ * <p>See {@code chapter29/docs/29-grammar.md} for the production-to-method map.
+ * Evaluation order and scope lifetimes are explicit: loop updates and declared
+ * constructor bodies are the two deferred source fragments. There is no AST.
+ */
+package com.seaofnodes.simple.parser;
diff --git a/src/main/java/com/seaofnodes/simple/type/Type.java b/src/main/java/com/seaofnodes/simple/type/Type.java
index 11e847d8..0f655f49 100644
--- a/src/main/java/com/seaofnodes/simple/type/Type.java
+++ b/src/main/java/com/seaofnodes/simple/type/Type.java
@@ -1,5 +1,6 @@
package com.seaofnodes.simple.type;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.GlobalBits;
import com.seaofnodes.simple.util.*;
import java.util.*;
diff --git a/src/main/java/com/seaofnodes/simple/type/TypeMem.java b/src/main/java/com/seaofnodes/simple/type/TypeMem.java
index cd0ae5d7..5faa7107 100644
--- a/src/main/java/com/seaofnodes/simple/type/TypeMem.java
+++ b/src/main/java/com/seaofnodes/simple/type/TypeMem.java
@@ -79,6 +79,15 @@ public class TypeMem extends Type {
public static TypeMem makePrivate(Type t) { return make(1,t,true,false,false,escapeFIDX(XInt.EMPTY,t), escapeAlias(XInt.EMPTY,t)); }
public TypeMem makeFrom(int alias) { return make(alias,_t,_one,_clz,_final,_escFs,_escAs); }
+ // Widen numeric fields at cyclic memory merges just as scalar Phis do.
+ public TypeMem widen() {
+ if( !_one || !(_t instanceof TypeStruct ts) ) return this;
+ TypeStruct wide = ts;
+ for( Field fld : ts._fields )
+ if( fld._t instanceof TypeInteger ti && !ti.isHigh() && !ti.isConstant() )
+ wide = wide.replace(fld.makeFrom(ti.same_but_slightly_wider_than()));
+ return wide==ts ? this : make(_alias,wide,_one,_clz,_final,_escFs,_escAs);
+ }
public TypeMem makeFrom(int[] fidxes, int[] aliases) {
return make(_alias,_t,_one,_clz,_final,fidxes,aliases);
}
diff --git a/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java b/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java
index 175b1c63..d9e11f8f 100644
--- a/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java
+++ b/src/main/java/com/seaofnodes/simple/type/TypeMemPtr.java
@@ -29,13 +29,16 @@ public class TypeMemPtr extends TypeNil {
public TypeStruct _obj;
// Deep read-only access through this pointer; independent of slot finality.
public boolean _ro;
+ // This reference may still depend on unfinished construction. Independent
+ // two-point lattice: meet ORs the restriction, dual complements it.
+ public boolean _partial;
public boolean _one; // Singleton pointer value; the pointer itself is constant.
public boolean _pub; // Escaped/public pointer. False means constructor-private.
private static final Ary<TypeMemPtr> FREE = new Ary<>(TypeMemPtr.class);
private TypeMemPtr(byte nil, TypeStruct obj, boolean one, boolean pub) { super(TMEMPTR,nil); init(nil,obj,one,pub); }
- private TypeMemPtr init(byte nil, TypeStruct obj, boolean one, boolean pub) { _nil=nil; _obj=obj; _ro=obj==TypeStruct.BOT; _one=one; _pub=pub; return this; }
+ private TypeMemPtr init(byte nil, TypeStruct obj, boolean one, boolean pub) { _nil = nil; _obj = obj; _ro = obj==TypeStruct.BOT; _partial = obj==TypeStruct.BOT; _one = one; _pub = pub; return this; }
// Return a filled-in TypeMemPtr; either from free list or alloc new.
static TypeMemPtr malloc(byte nil, TypeStruct obj, boolean one, boolean pub) {
return FREE.isEmpty() ? new TypeMemPtr(nil,obj,one,pub) : FREE.pop().init(nil,obj,one,pub);
@@ -46,10 +49,10 @@ public class TypeMemPtr extends TypeNil {
return make(nil,obj,one,true);
}
public static TypeMemPtr make(byte nil, TypeStruct obj, boolean one, boolean pub) {
- return make(nil,obj,one, pub,obj==TypeStruct.BOT);
+ return make(nil,obj,one,pub,obj==TypeStruct.BOT,obj==TypeStruct.BOT);
}
- private static TypeMemPtr make(byte nil, TypeStruct obj, boolean one, boolean pub, boolean ro) {
- TypeMemPtr tmp = malloc(nil, obj, one, pub).access(ro);
+ private static TypeMemPtr make(byte nil, TypeStruct obj, boolean one, boolean pub, boolean ro, boolean partial) {
+ TypeMemPtr tmp = malloc(nil, obj, one, pub).access(ro).partial(partial);
TypeMemPtr t2 = tmp.intern();
if( t2==tmp ) return tmp;
return VISIT.isEmpty() ? t2.free(tmp) : t2.delayFree(tmp);
@@ -72,21 +75,52 @@ public class TypeMemPtr extends TypeNil {
public static TypeMemPtr makeNullable(TypeStruct obj) { return make((byte)3, obj); }
public static TypeMemPtr makePrivate (TypeStruct obj) { return make((byte)2, obj, false, false); }
- public TypeMemPtr makeFrom(TypeStruct obj) { return obj==_obj ? this : make(_nil, obj, _one, _pub,_ro); }
+ public TypeMemPtr makeFrom(TypeStruct obj) { return obj==_obj ? this : make(_nil, obj, _one, _pub,_ro,_partial); }
public TypeMemPtr makeNullable() { return makeFrom((byte)3); }
/** Inferred mutable local: nullable, public and non-singleton. */
public TypeMemPtr makeVar() { return (TypeMemPtr)makeStorage(); }
/** Public-memory shape: nullable and non-singleton, preserving the referent. */
- @Override TypeMemPtr _makeStorage() { return make(_nil,_obj._makeStorage(),false,true,_ro); }
- @Override TypeMemPtr makeFrom(byte nil) { return nil==_nil ? this : make(nil, _obj, _one, _pub,_ro); }
+ @Override TypeMemPtr _makeStorage() { return make(_nil,_obj._makeStorage(),false,true,_ro,_partial); }
+ @Override TypeMemPtr makeFrom(byte nil) { return nil==_nil ? this : make(nil, _obj, _one, _pub,_ro,_partial); }
@Override public Type nonZero() { return makeFrom((byte)(_nil <= 1 ? 1 : 2)); }
private TypeMemPtr access(boolean ro) { _ro=ro; return this; }
+ private TypeMemPtr partial(boolean partial) { _partial = partial; return this; }
+
+ public TypeMemPtr withPartial(boolean partial) {
+ return partial==_partial ? this : make(_nil,_obj,_one,_pub,_ro,partial);
+ }
+
+ // A partial declaration promises no initialized slots. Actual callers may
+ // supply a sharper shape; the declaration must not manufacture those facts.
+ public TypeMemPtr partialBound() { return _partial ? makeFrom(_obj.makeInit()) : this; }
+
+ // Project field initialization onto declared field types, not stored
+ // constants. A pointer field being set does not complete its referent.
+ public TypeMemPtr initialized(TypeStruct state) {
+ TypeStruct shape = _obj;
+ for( Field fld : _obj._fields ) {
+ Field actual = state.field(fld._fname);
+ Type t = actual==null ? TypeScalar.BOT : actual._t;
+ if( t==Type.BOTTOM || t==TypeScalar.BOT ) t = TypeScalar.BOT;
+ else if( t.isHigh() ) t = TypeScalar.TOP;
+ else t = fld._t==Type.BOTTOM ? TypeScalar.BOT : fld._t==Type.TOP ? TypeScalar.TOP : fld._t;
+ shape = shape.replace(fld.makeFrom(t));
+ }
+ return makeFrom(shape);
+ }
+
+ // Conservative on unresolved scalar/pointer families; null is complete.
+ public static boolean mayBePartial(Type t) {
+ return t instanceof TypeMemPtr ptr ? ptr._partial
+ : t==Type.BOTTOM || t==TypeScalar.BOT || (t instanceof TypePtr && !t.isHigh());
+ }
public TypeMemPtr withAccess(boolean ro) {
if( _ro==ro ) return this;
TypeMemPtr ptr = malloc(_nil, _obj, _one, _pub);
ptr._ro = ro;
+ ptr._partial = _partial;
TypeMemPtr res = ptr.intern();
if( res==ptr ) return ptr;
return VISIT.isEmpty() ? res.free(ptr) : res.delayFree(ptr);
@@ -109,7 +143,7 @@ public class TypeMemPtr extends TypeNil {
public static final TypeMemPtr NOTBOT = make((byte)2,TypeStruct.BOT);
public static final TypeMemPtr TEST= make((byte)2, TypeStruct.TEST);
- public static void gather(ArrayList<Type> ts) { ts.add(NOTBOT); ts.add(BOT); ts.add(TEST); ts.add(TEST.makeRO()); }
+ public static void gather(ArrayList<Type> ts) { ts.add(NOTBOT); ts.add(BOT); ts.add(TEST); ts.add(TEST.makeRO()); ts.add(TEST.withPartial(true)); ts.add(TEST.withPartial(true).makeRO()); }
@Override
public TypeNil xmeet(Type t) {
@@ -118,28 +152,28 @@ public class TypeMemPtr extends TypeNil {
// Can't keep _one if mixing two unequal singletons; the result is not
// either singleton. Once either side is public, the meet is public.
boolean one = _one && that._one && (_obj==that._obj || !(_obj.isConAry() || that._obj.isConAry()));
- return make(xmeet0(that), (TypeStruct)_obj.meet(that._obj), one, _pub | that._pub,_ro | that._ro);
+ return make(xmeet0(that), (TypeStruct)_obj.meet(that._obj), one, _pub | that._pub,_ro | that._ro, _partial | that._partial);
}
@Override
- TypeMemPtr xdual() { return malloc( dual0(), _obj.dual(), !_one, !_pub).access(!_ro); }
+ TypeMemPtr xdual() { return malloc( dual0(), _obj.dual(), !_one, !_pub).access(!_ro).partial(!_partial); }
@Override TypeMemPtr rdual() {
if( _dual!=null ) return dual();
assert !_terned;
- TypeMemPtr d = malloc(dual0(), null, !_one, !_pub).access(!_ro);
+ TypeMemPtr d = malloc(dual0(), null, !_one, !_pub).access(!_ro).partial(!_partial);
(_dual = d)._dual = this; // Cross link duals
d._obj = _obj._terned ? _obj.dual() : _obj.rdual();
return d;
}
// RHS is NIL; do not deep-dual when crossing the center line
- @Override Type meet0() { return _nil==3 ? this : make((byte)3,_obj,false,true,_ro); }
+ @Override Type meet0() { return _nil==3 ? this : make((byte)3,_obj,false,true,_ro,_partial); }
@Override boolean _isConstant() { return _one; }
@Override boolean _isFinal() { return _ro; }
@Override TypeMemPtr _makeRO() { return withAccess(true); }
- @Override TypeMemPtr _close( String name, HashMap<String, Type> TYPES ) { return malloc(_nil,_obj._close(name, TYPES ),_one,_pub).access(_ro); }
+ @Override TypeMemPtr _close( String name, HashMap<String, Type> TYPES ) { return malloc(_nil,_obj._close(name, TYPES ),_one,_pub).access(_ro).partial(_partial); }
@Override Type _upgradeType(HashMap<String,Type> TYPES) {
return makeFrom((TypeStruct)_obj._upgradeType(TYPES));
@@ -147,47 +181,48 @@ public class TypeMemPtr extends TypeNil {
@Override public int log_size() { return 3; } // (1<<3)==8-byte pointers
- @Override int hash() { return (_ro ? 8192 : 0) ^ _obj.hashCode() ^ super.hash() ^ (_one ? 2048 : 0) ^ (_pub ? 4096 : 0); }
+ @Override int hash() { return (_partial ? 16384 : 0) ^ (_ro ? 8192 : 0) ^ _obj.hashCode() ^ super.hash() ^ (_one ? 2048 : 0) ^ (_pub ? 4096 : 0); }
@Override boolean eq(Type t) {
TypeMemPtr ptr = (TypeMemPtr)t; // Invariant
- return _ro==ptr._ro && super.eq(ptr) && _one == ptr._one && _pub == ptr._pub && _obj == ptr._obj;
+ return _partial==ptr._partial && _ro==ptr._ro && super.eq(ptr) && _one == ptr._one && _pub == ptr._pub && _obj == ptr._obj;
}
@Override boolean cycle_eq(Type t) {
if( t._type != TMEMPTR ) return false;
TypeMemPtr ptr = (TypeMemPtr)t; // Invariant
- return _ro==ptr._ro && super.eq(ptr) && _one == ptr._one && _pub == ptr._pub && _obj.cycle_eq(ptr._obj);
+ return _partial==ptr._partial && _ro==ptr._ro && super.eq(ptr) && _one == ptr._one && _pub == ptr._pub && _obj.cycle_eq(ptr._obj);
}
@Override public int nkids() { return 1; }
@Override public Type at( int idx ) { return _obj; }
@Override public void set( int idx, Type t ) { _obj = (TypeStruct)t; }
- // Reserve tags for null/not, one/general, public/private and read-only access
- @Override int TAGOFF() { return 16; }
+ // Reserve tags for null/not, one/general, public/private and read-only and incomplete access
+ @Override int TAGOFF() { return 32; }
@Override public void packed( BAOS baos, HashMap<String,Integer> strs ) {
assert _nil>=2;
baos.write(TAGOFFS[_type]
+ (_nil==2 ? 0 : 1)
+ (!_one ? 0 : 2)
+ (!_pub ? 0 : 4)
- + (!_ro ? 0 : 8) );
+ + (!_ro ? 0 : 8)
+ + (!_partial ? 0 : 16) );
}
static TypeMemPtr packed( int tag, BAOS bais ) {
- return malloc((byte)((tag&1)+2),null,(tag&2)==2,(tag&4)==4).access((tag&8)==8);
+ return malloc((byte)((tag&1)+2),null,(tag&2)==2,(tag&4)==4).access((tag&8)==8).partial((tag&16)==16);
}
@Override public String str() {
if( this==BOT ) return "PtrBot";
if( this==TOP ) return "PtrTop";
if( this== NOTBOT) return "*void";
- return x()+"*"+(_obj==null?"---":_obj.str())+q();
+ return (_partial ? "^" : "")+x()+"*"+(_obj==null?"---":_obj.str())+q();
}
@Override SB _print(SB sb, BitSet visit, boolean html ) {
if( this==BOT ) return sb.p("PtrBot");
if( this==TOP ) return sb.p("PtrTop");
if( this== NOTBOT) return sb.p("*void");
- return _obj.print(sb.p(x()).p("*"),visit,html).p(q());
+ return _obj.print(sb.p(_partial ? "^" : "").p(x()).p("*"),visit,html).p(q());
}
}
diff --git a/src/main/java/com/seaofnodes/simple/type/TypeStruct.java b/src/main/java/com/seaofnodes/simple/type/TypeStruct.java
index 7574022a..7c9758bd 100644
--- a/src/main/java/com/seaofnodes/simple/type/TypeStruct.java
+++ b/src/main/java/com/seaofnodes/simple/type/TypeStruct.java
@@ -16,10 +16,10 @@ public class TypeStruct extends Type {
/**
* `_open` and `_fref` are independent lattice coordinates.
*
- * `_open` says the structural tail is not known yet. On the low side an
- * open struct has infinitely many implicit BOT fields; its dual has an
- * implicit TOP tail. A real declaration is therefore open while its body
- * is being parsed:
+ * `_open` says the structural tail is not known yet - more fields may be
+ * added. On the low side an open struct has infinitely many implicit BOT
+ * fields; its dual has an implicit TOP tail. A real declaration is open
+ * while its body is being parsed:
*
* ```text
* struct S { int x; /* more declared fields may follow *\/ }
@@ -27,7 +27,8 @@ public class TypeStruct extends Type {
*
* `_fref` says no actual declaration has contributed to the type. The
* parser may still invent fields demanded by uses, leaving a useful open
- * structural constraint:
+ * structural constraint. Here `S` is an unknown struct, which has an
+ * unknown `x` field:
*
* ```text
* S? p; p.x; // fref S { BOT x; ... }
@@ -102,7 +103,7 @@ public class TypeStruct extends Type {
for( Field f : _fields )
if( f._fname.charAt(0) != '_' && // Skip private fields.
!hiddenInit(f) && // Skip private allocation helper.
- f._t != Type.TOP ) // Skip uninit fields
+ f._t != Type.TOP && f._t != TypeScalar.TOP ) // Skip unreachable values
xs[XInt.idx(f._alias)] |= XInt.mask(f._alias);
return (_aliases=XInt.intern(xs));
}
@@ -115,15 +116,15 @@ public class TypeStruct extends Type {
assert _terned;
int[] xs = XInt.EMPTY;
for( Field f : _fields )
- if( f._fname.charAt(0) != '_' && // Skip private fields.
- !hiddenInit(f) && // Skip private allocation helper.
- f._t instanceof TypeFunPtr tfp ) // Looking for TFP
- xs = XInt.meet(xs,tfp._fidxs);
+ if( f._fname.charAt(0) != '_' && !hiddenInit(f) ) {
+ if( f._t==TypeScalar.BOT || f._t==Type.BOTTOM ) return (_fidxs = XInt.FULL);
+ if( f._t instanceof TypeFunPtr tfp ) xs = XInt.meet(xs,tfp._fidxs);
+ }
return (_fidxs = xs);
}
- // Initializer/constructor helpers receive private memory. Only the
- // allocator has a source-callable signature.
+ // Hidden initializer/constructor helpers receive private memory. Only the
+ // allocator has a source-callable signature; TypeMem is not a source type.
public boolean hiddenInit(Field fld) {
return fld._t instanceof TypeFunPtr tfp && tfp.nargs()>=2 &&
tfp.arg(1) instanceof TypeMem;
@@ -153,8 +154,7 @@ public class TypeStruct extends Type {
Field[] fs = new Field[_fields.length];
for( int i=0; i<_fields.length; i++ ) {
if( _fields[i]._extern ) { fs[i] = _fields[i]; continue; }
- Type zero = _fields[i]._t.makeZero();
- fs[i] = _fields[i].makeFrom(_fields[i]._t != Type.BOTTOM && zero.isa(_fields[i]._t) ? zero : Type.TOP);
+ fs[i] = _fields[i].makeFrom(TypeScalar.BOT);
}
return make(_name,_open,_fref,fs);
}
@@ -650,4 +650,19 @@ public class TypeStruct extends Type {
return is;
}
+
+ // Class-object name, distinct from its instance type.
+ public static final String CLZ = "class:";
+
+ public static String addClzPrefix( String x ) {
+ assert !x.startsWith(CLZ);
+ return (CLZ+x).intern();
+ }
+ public static boolean startsClzPrefix( String s ) {
+ return s.startsWith(CLZ);
+ }
+ public static boolean isPublicClassName( String name ) {
+ if( startsClzPrefix(name) ) name = name.substring(CLZ.length());
+ return !name.isEmpty() && name.charAt(0)!='_' && name.indexOf("._")==-1;
+ }
}
diff --git a/src/test/java/com/seaofnodes/simple/ArithNodeTest.java b/src/test/java/com/seaofnodes/simple/ArithNodeTest.java
index e912f224..1e6c6fc0 100644
--- a/src/test/java/com/seaofnodes/simple/ArithNodeTest.java
+++ b/src/test/java/com/seaofnodes/simple/ArithNodeTest.java
@@ -9,6 +9,30 @@ import org.junit.Test;
import static org.junit.Assert.*;
public class ArithNodeTest {
+ @Test public void testConstantFoldsRequireInitializedInputs() {
+ new CodeGen("return 0;");
+ Node x = ConstantNode.seed(TypeScalar.BOT);
+ Node zero = ConstantNode.seed(TypeInteger.ZERO);
+ for( Node op : new Node[]{new MulNode(x,zero,(byte)1),new AndNode(null,x,zero),
+ new SubNode(x,x,(byte)1),new BoolNode.EQ(x,x,(byte)1),
+ new BoolNode.NE(x,x,(byte)1),new BoolNode.ULT(x,x),new NotNode(x),
+ new ConvertNode(TypeInteger.BOT,x),new ToIntegerNode(x),new ToFloatNode(x),new PtrToIntNode(x)} ) {
+ assertSame(op.label(),TypeScalar.BOT,op.compute());
+ Node replacement = op.idealize();
+ assertTrue(op.label(),replacement==null || !replacement.compute().isConstant());
+ }
+ // Once the family is known, compute owns the annihilator fold.
+ x._type = TypeInteger.BOT;
+ assertSame(TypeInteger.ZERO,new MulNode(x,zero,(byte)1).compute());
+ assertSame(TypeInteger.TRUE,new BoolNode.EQ(x,x,(byte)1).compute());
+ assertSame(TypeInteger.FALSE,new BoolNode.NE(x,x,(byte)1).compute());
+ assertSame(TypeInteger.FALSE,new BoolNode.ULT(x,x).compute());
+ x._type = TypeFloat.F64;
+ assertSame(TypeInteger.BOOL,new BoolNode.EQ(x,x,(byte)2).compute());
+ x._type = TypeFloat.constant(Double.NaN);
+ assertSame(TypeInteger.FALSE,new BoolNode.EQ(x,x,(byte)2).compute());
+ }
+
@Test public void testUnresolvedInput() {
new CodeGen("return 0;").driver(CodeGen.Phase.Opto);
Node lhs = ConstantNode.seed(TypeInteger.TRUE);
@@ -16,9 +40,9 @@ public class ArithNodeTest {
ArithNode add = new AddNode(lhs,rhs);
assertSame(TypeInteger.constant(2),add.compute());
lhs._type = TypeScalar.BOT;
- assertSame(Type.BOTTOM,add.compute());
+ assertSame(TypeScalar.BOT,add.compute());
add.setMode((byte)1);
- assertSame(Type.BOTTOM,add.compute());
+ assertSame(TypeScalar.BOT,add.compute());
}
@Test public void testFloatPrecision() {
@@ -26,6 +50,10 @@ public class ArithNodeTest {
assertEquals("1.6777217E7",Eval2.eval(code,16777216));
code = new CodeGen("f32 x = arg; f32 y = x+1.0; return y;").driver(CodeGen.Phase.TypeCheck);
assertEquals("1.6777216E7",Eval2.eval(code,16777216));
+ code = new CodeGen("flt x = 0.0; flt y = 0.0; if(arg) { x = 1.5; y = 3.5; } else { x = 2.5; y = 4.5; } return x+y;")
+ .driver(CodeGen.Phase.TypeCheck);
+ assertEquals("7.0",Eval2.eval(code,0));
+ assertEquals("5.0",Eval2.eval(code,1));
}
@Test public void testMonotoneCompute() {
diff --git a/src/test/java/com/seaofnodes/simple/BoundaryTest.java b/src/test/java/com/seaofnodes/simple/BoundaryTest.java
index e196b93f..10b7517e 100644
--- a/src/test/java/com/seaofnodes/simple/BoundaryTest.java
+++ b/src/test/java/com/seaofnodes/simple/BoundaryTest.java
@@ -1,5 +1,6 @@
package com.seaofnodes.simple;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.*;
@@ -23,10 +24,7 @@ public class BoundaryTest {
CodeGen code=new CodeGen(src).driver(CodeGen.Phase.LocalSched);
assertEquals("2018",Eval2.eval(code,19));
assertEquals("0",Eval2.eval(code,0));
- code._stop.walk(n -> {
- if( n instanceof StoreNode st ) assertEquals(0,st.cfg0().loopDepth());
- return null;
- });
+ // Private memory follows the loop; stores need not move to its exit.
code=new CodeGen("struct Byte { new Byte={int n -> self.x=n; self.x+=3;}; u8 x; }; return new Byte(255).x;").driver(CodeGen.Phase.TypeCheck);
assertEquals("2",Eval2.eval(code,0));
// data is still unset when the constructor reads and updates x.
diff --git a/src/test/java/com/seaofnodes/simple/CallEndTest.java b/src/test/java/com/seaofnodes/simple/CallEndTest.java
index a174bdf7..f57fd96a 100644
--- a/src/test/java/com/seaofnodes/simple/CallEndTest.java
+++ b/src/test/java/com/seaofnodes/simple/CallEndTest.java
@@ -7,6 +7,60 @@ import org.junit.Test;
import static org.junit.Assert.*;
public class CallEndTest {
+ // An unknown value can become a private object or callback. Both escape
+ // sinks must reserve those possibilities before the value is known.
+ @Test public void testUnknownEscapesRefineToPrivateValues() {
+ CodeGen code = new CodeGen("return 0;").parse();
+ code._phase = CodeGen.Phase.IterPostOpto;
+ StopCUNode stop = code.compunit()._stop;
+ stop._type = TypeTuple.make(Type.CONTROL,TypeMem.START,Type.BOTTOM);
+ ReturnNode ret = (ReturnNode)stop.in(0);
+ TypeFunPtr callback = TypeFunPtr.make1((byte)2,true,TypeFunPtr.TEMPTY,TypeInteger.BOT,100);
+ TypeMemPtr object = TypeMemPtr.make(TypeStruct.make("_Private",false,
+ Field.make("callback",callback,100,false)));
+ code.externFunc(101,"retain");
+ TypeFunPtr retain = TypeFunPtr.make1((byte)2,true,
+ new Type[]{TypeScalar.BOT},TypeInteger.BOT,101);
+ Node value = ConstantNode.raw(TypeScalar.BOT);
+ CallNode call = new CallNode(null,ConstantNode.raw(Type.CONTROL),
+ ConstantNode.raw(TypeMem.START),value,ConstantNode.raw(retain));
+ call._type = Type.CONTROL;
+ GlobalEscapeNode global = new GlobalEscapeNode();
+ global._type = TypeMem.START;
+ for( Type broad : new Type[]{Type.BOTTOM,TypeScalar.BOT} ) {
+ value._type = broad;
+ ret._type = TypeTuple.make(Type.CONTROL,TypeMem.START,broad);
+ TypeTuple unknownStop = stop.compute();
+ TypeMem unknownGlobal = global.compute();
+ assertSame(XInt.FULL,((TypeMem)unknownStop.mem())._escFs);
+ assertSame(XInt.FULL,((TypeMem)unknownStop.mem())._escAs);
+ assertSame(XInt.FULL,unknownGlobal._escFs);
+ assertSame(XInt.FULL,unknownGlobal._escAs);
+ for( Type precise : new Type[]{object,callback} ) {
+ assertTrue(precise.isa(broad));
+ value._type = precise;
+ ret._type = TypeTuple.make(Type.CONTROL,TypeMem.START,precise);
+ assertTrue(stop.compute().isa(unknownStop));
+ assertTrue(global.compute().isa(unknownGlobal));
+ }
+ }
+ }
+
+ @Test public void testUnresolvedOffsetInInlineBody() {
+ CodeGen code = new CodeGen("val f = { int[] data -> return data#; }; return f;").parse();
+ for( FunNode fun : code._linker )
+ if( fun!=null && "f".equals(fun._name) ) {
+ // Keep the offset unresolved: an early inline must copy it
+ // with its data parameter, not share it with the old body.
+ FldOffNode off = new FldOffNode(fun.parm(3),"<<",null).init();
+ fun.ret().setDef(2,off);
+ assertSame(Type.BOTTOM,off._type);
+ assertTrue("Unresolved field offset belongs to the function",fun.body().get(off._nid));
+ return;
+ }
+ fail("Missing test function");
+ }
+
@Test public void testReturnWideningMonotonicity() {
new CodeGen("return 0;").parse();
TypeFunPtr sig = TypeFunPtr.make1((byte)2,true,TypeFunPtr.TEMPTY,TypeInteger.I32,100);
diff --git a/src/test/java/com/seaofnodes/simple/Chapter10Test.java b/src/test/java/com/seaofnodes/simple/Chapter10Test.java
index 165eba21..eadaf9fe 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter10Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter10Test.java
@@ -1,5 +1,6 @@
package com.seaofnodes.simple;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
diff --git a/src/test/java/com/seaofnodes/simple/Chapter12Test.java b/src/test/java/com/seaofnodes/simple/Chapter12Test.java
index 973b15d9..c6817308 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter12Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter12Test.java
@@ -79,6 +79,19 @@ return f0.i.f.i.i;
assertEquals("return 17;", code.print());
}
+ @Test public void testPrivateRecursiveReturn() {
+ CodeGen code = new CodeGen("""
+ struct _A { !Test._B? !b; };
+ struct _B { !_A? !a; };
+ !_A !_a = new _A;
+ !_B !_b = new _B;
+ _a.b = _b;
+ _b.a = _a;
+ return _a.b.a.b;
+ """).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("return Test._B;",code.print());
+ }
+
@Test
public void testNullRef0() {
CodeGen code = new CodeGen(
diff --git a/src/test/java/com/seaofnodes/simple/Chapter17bTest.java b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java
index 4019fd38..98197b77 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter17bTest.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java
@@ -1,5 +1,6 @@
package com.seaofnodes.simple;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
import org.junit.Ignore;
import org.junit.Test;
@@ -292,7 +293,9 @@ struct A { !B !b; new A = { ->
A x = new A;
""");
try { code.driver(CodeGen.Phase.TypeCheck); fail(); }
- catch( Exception e ) { assertEquals("Field 'b' might not be initialized",e.getMessage()); }
+ // The unused constructor return value can die; its unwritten field
+ // still fails the completion check at TypeCheck.
+ catch( Exception e ) { assertEquals("'Test.A' is not fully initialized, field 'b' is only partially set in the constructor",e.getMessage()); }
}
@Test
@@ -304,7 +307,7 @@ A x = new A;
"new A = { -> }; !B !b;", // Field declared later
} ) {
CodeGen code = new CodeGen("struct B {}; struct A { "+fields+" }; return 0;");
- try { code.parse(); fail(fields); }
+ try { code.driver(CodeGen.Phase.TypeCheck); fail(fields); }
catch( Parser.ParseException e ) { assertEquals("'Test.A' is not fully initialized, field 'b' is only partially set in the constructor",e.getMessage()); }
}
}
@@ -313,7 +316,7 @@ A x = new A;
public void testDefaultConstructorFields() {
for( String field : new String[] { "!B !b;", "!B !b;" } ) {
CodeGen code = new CodeGen("struct B {}; struct A { "+field+" }; return new A;");
- try { code.parse(); fail(field); }
+ try { code.driver(CodeGen.Phase.TypeCheck); fail(field); }
catch( Parser.ParseException e ) { assertEquals("'Test.A' is not fully initialized, field 'b' needs to be set in a constructor",e.getMessage()); }
}
for( String fields : new String[] {
@@ -549,7 +552,7 @@ struct _A {
};
return new _A;
""");
- code.driver(CodeGen.Phase.FinalizeIR);
+ code.driver(CodeGen.Phase.TypeCheck);
assertEquals("return Test._A;", code.print());
assertEquals("Test._A{f1=null,f2=null}", Eval2.eval(code,0));
}
@@ -564,7 +567,7 @@ struct _A {
};
return new _A(new !B?[0]).array_of_b;
""");
- code.driver(CodeGen.Phase.FinalizeIR);
+ code.driver(CodeGen.Phase.TypeCheck);
assertEquals("return []*B?;", code.print());
assertEquals("*B {}?[]", Eval2.eval(code,0));
}
diff --git a/src/test/java/com/seaofnodes/simple/Chapter18Test.java b/src/test/java/com/seaofnodes/simple/Chapter18Test.java
index 8e109991..ecb2b934 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter18Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter18Test.java
@@ -1,4 +1,6 @@
package com.seaofnodes.simple;
+import com.seaofnodes.simple.parser.Var;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.type.*;
import com.seaofnodes.simple.node.*;
diff --git a/src/test/java/com/seaofnodes/simple/Chapter19Test.java b/src/test/java/com/seaofnodes/simple/Chapter19Test.java
index 3cc00922..50ca8922 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter19Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter19Test.java
@@ -118,7 +118,7 @@ val hashCode = { !String self ->
hashCode(new String("Hello, World!"));
""";
CodeGen code = new CodeGen(src).driver(Phase.LocalSched);
- assertEquals("Stop[ return Phi(Region,123456789,(!=0)Phi(Loop,0,(.[]+((Phi_hash<<5)-Phi_hash)))); return Test.String; return Phi(Region,0,1,0,1); return Phi(Region,(!=0)._hashCode,Phi(Region,123456789,(!=0)Phi(Loop,0,(.[]+((Phi_hash<<5)-Phi_hash))))); ]", code.print());
+ assertEquals("Stop[ return Phi(Region,123456789,(!=0)Phi(Loop,0,(((Phi_hash<<5)-Phi_hash)+.[]))); return Test.String; return Phi(Region,0,1,0,1); return Phi(Region,(!=0)._hashCode,Phi(Region,123456789,(!=0)Phi(Loop,0,(((Phi_hash<<5)-Phi_hash)+.[])))); ]", code.print());
assertEquals("4029215624828139541", Eval2.eval(code, 2));
}
diff --git a/src/test/java/com/seaofnodes/simple/Chapter20Test.java b/src/test/java/com/seaofnodes/simple/Chapter20Test.java
index 7eb1f255..f5eb2b36 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter20Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter20Test.java
@@ -79,6 +79,7 @@ return f(s);
@Test public void testAllocatorUnion() { com.seaofnodes.simple.codegen.RegAllocTestSupport.union(); }
@Test public void testAllocatorCopyClobber() throws Exception { com.seaofnodes.simple.codegen.RegAllocTestSupport.copyClobber(); }
@Test public void testAllocatorCommutativePhi() { com.seaofnodes.simple.codegen.RegAllocTestSupport.commutativePhi(); }
+ @Test public void testAllocatorCommutativeLoop() { com.seaofnodes.simple.codegen.RegAllocTestSupport.commutativeLoop(); }
@Test public void testAllocatorKills() { com.seaofnodes.simple.codegen.RegAllocTestSupport.killWithoutResult(); }
@Test public void testAllocatorDependencies() { com.seaofnodes.simple.codegen.RegAllocTestSupport.nullUseMask(); }
@Test public void testAllocatorCloneClass() { com.seaofnodes.simple.codegen.RegAllocTestSupport.cloneRegisterClass(); }
@@ -255,6 +256,7 @@ val hashCode = { !String self ->
? self._hashCode
: (self._hashCode = _hashCodeString(self));
};
+
""";
// Return the public hashCode wrapper; keep its helper private.
testCPU(src,"x86_64_v2", "SystemV",12,null);
diff --git a/src/test/java/com/seaofnodes/simple/Chapter22Test.java b/src/test/java/com/seaofnodes/simple/Chapter22Test.java
index 00d77f65..e9a668a7 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter22Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter22Test.java
@@ -1,5 +1,6 @@
package com.seaofnodes.simple;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.isa.eval.EvalRisc5;
import com.seaofnodes.isa.eval.EvalArm64;
diff --git a/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/src/test/java/com/seaofnodes/simple/Chapter25Test.java
index 1ad1e129..5f615282 100644
--- a/src/test/java/com/seaofnodes/simple/Chapter25Test.java
+++ b/src/test/java/com/seaofnodes/simple/Chapter25Test.java
@@ -5,6 +5,7 @@ import com.seaofnodes.isa.eval.EvalRisc5;
import com.seaofnodes.isa.eval.EvalArm64;
import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen;
import com.seaofnodes.simple.codegen.ElfReader;
import com.seaofnodes.simple.codegen.ParseAll;
diff --git a/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java b/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java
index dfd7a78c..dae64fb0 100644
--- a/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java
+++ b/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java
@@ -1,5 +1,6 @@
package com.seaofnodes.simple;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.*;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.Type;
diff --git a/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java b/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
index e2d822d0..285d4293 100644
--- a/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
+++ b/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java
@@ -1,5 +1,6 @@
package com.seaofnodes.simple;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
import org.junit.Test;
import static org.junit.Assert.*;
diff --git a/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java b/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java
new file mode 100644
index 00000000..e544fcad
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java
@@ -0,0 +1,162 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.parser.Parser;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class ConstructionGroupTest {
+ private static final String CHILD = """
+ struct Child {
+ Test.Parent? parent;
+ int value = 5;
+ new Child = { Test.Parent? ^par -> parent = par; };
+ };
+ val read = { Child c -> return c.value; };
+ """;
+
+ private String parent(String body) {
+ return CHILD+"struct Parent { Child child; int~ payload; new Parent = { int n -> "+body+" }; };";
+ }
+
+ private void rejects(String src, String message) {
+ try {
+ new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
+ fail("Accepted: "+src);
+ } catch( Parser.ParseException e ) {
+ assertTrue(e.getMessage(),e.getMessage().contains(message));
+ }
+ }
+
+ @Test public void testCyclesAndIndependentAllocations() {
+ String src = parent("val detached = new Child(null); payload = n+read(detached); child = new Child(self);")+
+ "val p = new Parent(arg); val local = new Child(p); val par = local.parent; return (par ? par.payload : 0)+p.child.value;";
+ for( long seed : new long[]{1,123,987654321} ) {
+ CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("17",Eval2.eval(code,7));
+ assertEquals("10",Eval2.eval(code,0));
+ }
+ new CodeGen(src.replace("Child","Child_noInline"))
+ .driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION,true,false,0);
+ }
+
+ @Test public void testIncompleteChildReadAfterInlining() {
+ // Child's own slots are initialized, even though its parent remains
+ // partial. Reading that slot does not complete the parent reference.
+ String src = parent("child = new Child(self); payload = child.parent==null;")+
+ "return new Parent(arg).payload;";
+ assertEquals("0",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),0));
+ }
+
+ @Test public void testInitializerFeedsConstructor() {
+ // Private memory comes from the initializer's real call edge. An
+ // unknown-caller default must neither erase it nor poison its fields.
+ String src = "struct C { int !x = 7; new C = { -> x = x+1; }; }; return new C().x;";
+ for( long seed : new long[]{1,123,987654321} ) {
+ CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("8",Eval2.eval(code,0));
+ assertEquals("Stop[ return 8; return Test.C; ]",code.print());
+ }
+ }
+
+ @Test public void testNoIncompleteLookupOrEscape() {
+ rejects(parent("payload = 1; child = new Child(self); read(child);")+"return 0;","ordinary function");
+ rejects(parent("payload = 1; child = new Child(self);").replace("parent = par;","parent = par; value = par.payload;")+
+ "return 0;","Might be null");
+ rejects("""
+ struct Box { Test.C? value; }; val box = new Box;
+ struct C { int~ x; new C = { -> box.value = self; x = 1; }; };
+ return 0;
+ ""","ordinary object");
+ rejects("struct C { C next = self; int~ x; new C = { -> x = next.x; }; }; return new C().x;",
+ "might not be initialized");
+ }
+
+ @Test public void testAliasesAndLoopBackedges() {
+ String src = parent("val alias = self; alias.payload = n; child = new Child(alias);")+"return new Parent(arg).child.value;";
+ assertEquals("5",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),3));
+ rejects(parent("payload = 1; child = new Child(self); Child? !c = null; while(n--) { if(c) read(c); c = child; }")+
+ "return 0;","ordinary function");
+ rejects(parent("payload = 1; child = new Child(self); Parent? !p = null; while(n--) { val c = new Child(p); read(c); p = self; }")+
+ "return 0;","ordinary function");
+ rejects(parent("payload = 1; child = new Child(self); val c = n ? new Child(null) : child; read(c);")+
+ "return 0;","ordinary function");
+ }
+
+ @Test public void testPartialQualifier() {
+ new CodeGen("struct C {}; val f = { C ^c -> return 0; }; return 0;").driver(CodeGen.Phase.TypeCheck);
+ rejects("struct C { new C = { int ^x -> }; }; return 0;","struct parameter");
+ rejects(parent("payload = 1; child = new Child(self);").replace("^par","par")+"return 0;","without '^'");
+ CodeGen code = new CodeGen(parent("payload = 1; child = new Child(self);")+"return 0;")
+ .driver(CodeGen.Phase.TypeCheck);
+ try {
+ code.serialize();
+ fail("Exported a constructor qualifier without its contract");
+ } catch( Parser.ParseException e ) {
+ assertTrue(e.getMessage(),e.getMessage().contains("Separate compilation"));
+ }
+ }
+
+ @Test public void testCompleteLoopAllocations() {
+ // Optimistic types prove the cycle p -> new Link(p) -> p complete.
+ String src = """
+ struct Link {
+ Link? next;
+ int value = 5;
+ new Link = { Link? ^n -> next = n; };
+ };
+ struct Root {
+ Link head;
+ int value;
+ new Root = { int n ->
+ Link? !p = null;
+ while(n--) p = new Link(p);
+ if(!p) p = new Link(null);
+ value = p.value;
+ head = p;
+ };
+ };
+ val root = new Root(arg);
+ return root.value + root.head.value;
+ """;
+ CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("10",Eval2.eval(code,0));
+ assertEquals("10",Eval2.eval(code,3));
+ // Dead calls discard their arguments before a construction result exists.
+ assertEquals("0",Eval2.eval(new CodeGen(CHILD+"struct Parent {}; return 0; new Child(null);")
+ .driver(CodeGen.Phase.TypeCheck),0));
+ }
+
+ @Test public void testDeclaredQualifierAndOwnReceiver() {
+ // A public constructor's unknown callers promise no initialized slots.
+ for( String body : new String[]{"value = par.payload;", "value = par.payload*0;",
+ "int x = par.payload; value = x*0;", "value = par.payload==par.payload;"} )
+ rejects("""
+ struct Parent { int payload = 7; };
+ struct Child {
+ int value;
+ new Child = { Parent ^par -> BODY };
+ };
+ return new Child(new Parent).value;
+ """.replace("BODY",body),"might not be initialized");
+ // The self exception belongs to the receiver, not every pointer of its class.
+ rejects("""
+ struct C {
+ int !value;
+ new C = { !C? ^other -> if(other) other.value = 1; value = 2; };
+ };
+ return new C(new C(null)).value;
+ ""","incomplete object");
+ }
+
+ @Test public void testUnusedIncompleteReadCanDie() {
+ String src = """
+ struct Parent { int payload = 7; };
+ struct Child { int value = 5;
+ new Child = { Parent ^par -> int unused = par.payload; };
+ };
+ return new Child(new Parent).value;
+ """;
+ assertEquals("5",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),0));
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/ConstructionRequirementsTest.java b/src/test/java/com/seaofnodes/simple/ConstructionRequirementsTest.java
new file mode 100644
index 00000000..c570e643
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/ConstructionRequirementsTest.java
@@ -0,0 +1,222 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.parser.Parser;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+@org.junit.Ignore("Deferred: inferred field requirements and incomplete-object traversal")
+public class ConstructionRequirementsTest {
+ private static final String CHILD = """
+ struct Child {
+ Test.Parent parent;
+ int~ width;
+ new Child = { Test.Parent ^par ->
+ parent = par;
+ width = par.width;
+ };
+ };
+ """;
+
+ private String parent(String body) {
+ return CHILD+"struct Parent { Child child; int~ width; new Parent = { int n -> "+body.replace("arg","n")+" }; };";
+ }
+
+ private void rejects(String src, String message) {
+ try {
+ new CodeGen(src).parse();
+ fail("Accepted: "+src);
+ } catch( Parser.ParseException e ) {
+ assertTrue(e.getMessage(),e.getMessage().contains(message));
+ }
+ }
+
+ @Test public void testCycleAndRequiredField() {
+ String src = parent("width = arg+10; child = new Child(self);")+"return new Parent(arg).child.parent.child.width;";
+ CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("17",Eval2.eval(code,7));
+ assertEquals("10",Eval2.eval(code,0));
+ code = new CodeGen(parent("width = arg+10; child = new Child(self);")+
+ "val p = new Parent(arg); return new Child(p).parent.child.width;").driver(CodeGen.Phase.TypeCheck);
+ assertEquals("17",Eval2.eval(code,7));
+ code = new CodeGen(CHILD+"struct Parent { int width = 10; Child child = new Child(self); }; return new Parent().child.width;")
+ .driver(CodeGen.Phase.TypeCheck);
+ assertEquals("10",Eval2.eval(code,0));
+ rejects(parent("child = new Child(self); width = 10;")+"return 0;","requires initialized field 'width'");
+ rejects(parent("if(arg) width = 10; child = new Child(self); width = 20;")+"return 0;","requires initialized field 'width'");
+ code = new CodeGen(parent("child = new Child(self); width = 10;").replace("width = par.width;","width = 5;")+
+ "return new Parent(arg).child.parent.width;").driver(CodeGen.Phase.TypeCheck);
+ assertEquals("10",Eval2.eval(code,0));
+ // Even if the comparison folds using the non-null declared type,
+ // accessing child still requires that field to have been initialized.
+ rejects(parent("width = 10; child = new Child(self);").replace("width = par.width;","width = par.child==null;")+
+ "return 0;","requires initialized field 'child'");
+ }
+
+ @Test public void testNoEscapeOrFieldLaundering() {
+ rejects("struct C { C next = self; int~ payload; new C = { -> payload = next.payload; }; }; return 0;",
+ "might not be initialized");
+ CodeGen code = new CodeGen("struct C { C next = self; int payload; new C = { -> payload = 17; payload = next.payload; }; }; return new C().next.payload;")
+ .driver(CodeGen.Phase.TypeCheck);
+ assertEquals("17",Eval2.eval(code,0));
+ rejects("""
+ struct C {
+ C next; int~ payload;
+ new C = { -> next = self; payload = next.payload; };
+ }; return 0;
+ ""","might not be initialized");
+ rejects(parent("width = 10; child = new Child(self); use(child);")+
+ "val use = { Child c -> return c.parent.width; }; return 0;","ordinary function");
+ rejects(CHILD+"""
+ struct Parent {
+ Child child; int~ width;
+ new Parent = { -> width = 10; child = new Child(self); int x = child.parent.width; };
+ }; return 0;
+ ""","Traversing an unfinished reference field");
+ rejects("""
+ struct C {
+ int x;
+ val get = { -> return x; };
+ new C = { -> x = get(); };
+ }; return 0;
+ ""","ordinary function");
+ rejects("""
+ struct Box { Test.C? value; }; val box = new Box;
+ struct C { new C = { -> box.value = self; }; }; return 0;
+ ""","ordinary object");
+ }
+
+ @Test public void testConstructorOnlyQualifier() {
+ rejects("struct C {}; val f = { C ^c -> return 0; }; return 0;","constructor");
+ rejects("struct C { new C = { int ^x -> }; }; return 0;","struct parameter");
+ String src = parent("width = 10; child = new Child(self);")+"return 0;";
+ CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
+ try {
+ code.serialize();
+ fail("Exported an unrecorded construction contract");
+ } catch( Parser.ParseException e ) {
+ assertTrue(e.getMessage(),e.getMessage().contains("Separate compilation"));
+ }
+ }
+
+ @Test public void testTransitiveRequirementsAndAliases() {
+ String child = """
+ struct Leaf {
+ int~ width;
+ new Leaf = { Test.Parent ^par -> val alias = par; width = alias.width; };
+ };
+ struct Child {
+ Test.Parent parent; Leaf leaf;
+ new Child = { Test.Parent ^par -> parent = par; leaf = new Leaf(par); };
+ };
+ """;
+ String parent = "struct Parent { Child child; int~ width; new Parent = { int n -> %s }; };";
+ String good = child+parent.formatted("width = n+10; val alias = self; child = new Child(alias);");
+ CodeGen code = new CodeGen(good+"return new Parent(arg).child.leaf.width;").driver(CodeGen.Phase.TypeCheck);
+ assertEquals("17",Eval2.eval(code,7));
+ rejects(child+parent.formatted("child = new Child(self); width = 10;")+"return 0;",
+ "requires initialized field 'width'");
+ }
+
+ @Test public void testLoopsAndUninlinedConstructor() {
+ String src = parent("width = 1; for(int i = 0; i < arg; i++) width += 2; child = new Child(self);")+
+ "return new Parent(arg).child.width;";
+ for( long seed : new long[]{1,123,987654321} ) {
+ CodeGen code = new CodeGen(src.replace("Child","Child_noInline"),seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("7",Eval2.eval(code,3));
+ assertEquals("1",Eval2.eval(code,0));
+ }
+ new CodeGen(src.replace("Child","Child_noInline"))
+ .driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION,true,false,0);
+ rejects(parent("for(int i = 0; i < arg; i++) width = 2; child = new Child(self); width = 10;")+"return 0;",
+ "requires initialized field 'width'");
+ // The assignment on the backedge introduces the unfinished alias
+ // after the call was parsed. It must not pass as an ordinary Child.
+ rejects(parent("width = 10; child = new Child(self); Child? !c = null; while(arg--) { use(c); c = child; }")+
+ "val use = { Child? c -> return 0; }; return 0;","ordinary function");
+ }
+
+ @Test public void testNullableFieldAndDuplicateArguments() {
+ String src = """
+ struct Child {
+ Test.Parent parent; int~ width;
+ new Child = { Test.Parent ^a, Test.Parent ^b ->
+ parent = a; width = a.width+b.width+(a.optional==null);
+ };
+ };
+ struct Parent {
+ Child child; int width; Child? optional;
+ new Parent = { -> width = 10; child = new Child(self,self); };
+ };
+ return new Parent().child.width;
+ """;
+ CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("21",Eval2.eval(code,0));
+ }
+
+ @Test public void testNullableUnfinishedParameter() {
+ String child = """
+ struct Child {
+ Test.Parent? parent; int width;
+ new Child = { Test.Parent? ^par ->
+ parent = par; width = par ? par.width : 0;
+ };
+ };
+ val read = { Child c -> return c.width; };
+ """;
+ String parent = """
+ struct Parent {
+ Child child; int~ width;
+ new Parent = { int n ->
+ val detached = new Child(null);
+ width = n+read(detached);
+ child = new Child(self);
+ };
+ };
+ """;
+ CodeGen code = new CodeGen(child+parent+"""
+ val p = new Parent(arg);
+ val leaf = new Child(p);
+ return read(leaf)+read(p.child);
+ """).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("14",Eval2.eval(code,7));
+ assertEquals("0",Eval2.eval(code,0));
+ rejects(child+parent.replace("width = n+read(detached);","").replace("child = new Child(self);",
+ "child = new Child(self); width = n;"),"requires initialized field 'width'");
+ rejects(child+parent.replace("child = new Child(self);","child = new Child(self); read(child);"),
+ "ordinary function");
+ try {
+ new CodeGen(child.replace("par ? par.width : 0","par.width")+parent+
+ "return new Child(null).width;").driver(CodeGen.Phase.TypeCheck);
+ fail("Dereferenced null constructor argument");
+ } catch( Parser.ParseException e ) {
+ assertTrue(e.getMessage(),e.getMessage().toLowerCase().contains("null"));
+ }
+ }
+
+ @Test public void testDistinctCallTimeMemory() {
+ String src = CHILD+"""
+ struct Parent {
+ Child child; Child second; int~ width;
+ new Parent = { int n ->
+ width = n+1; child = new Child(self);
+ width = n+2; second = new Child(self);
+ width = n+3;
+ };
+ };
+ val p = new Parent(arg); return p.child.width*100+p.second.width*10+p.width;
+ """;
+ for( long seed : new long[]{1,123,987654321} ) {
+ CodeGen code = new CodeGen(src.replace("Child","Child_noInline"),seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("123",Eval2.eval(code,0));
+ assertEquals("456",Eval2.eval(code,3));
+ }
+ new CodeGen(src.replace("Child","Child_noInline"))
+ .driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION,true,false,0);
+ String loop = parent("width = 1; child = new Child(self); for(int i = 0; i < arg; i++) { width += 1; child = new Child(self); } width += 10;")+
+ "val p = new Parent(arg); return p.child.width*100+p.width;";
+ CodeGen code = new CodeGen(loop.replace("Child","Child_noInline")).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("414",Eval2.eval(code,3));
+ assertEquals("111",Eval2.eval(code,0));
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/ConstructorCompletionTest.java b/src/test/java/com/seaofnodes/simple/ConstructorCompletionTest.java
new file mode 100644
index 00000000..a5fc236b
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/ConstructorCompletionTest.java
@@ -0,0 +1,119 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.parser.Parser;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class ConstructorCompletionTest {
+ @Test public void testFieldMemoryDuringInitialization() {
+ CodeGen code = new CodeGen("""
+ struct A {
+ int x = 1;
+ x = 2;
+ int y = self.x;
+ new A = { -> self.x = 3; y += x; };
+ };
+ val a = new A; return a.x*10+a.y;
+ """).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("35",Eval2.eval(code,0));
+ code = new CodeGen("""
+ struct A {
+ int[] data = new int[3];
+ new A = { -> for(int i = 0; i < 3; i++) data[i] = i+1; };
+ };
+ return new A().data[2];
+ """).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("3",Eval2.eval(code,0));
+ for( String body : new String[]{
+ "int~ x; int y = x; new A = { -> x = 7; };",
+ "int~ x; int y = x*0; new A = { -> x = 7; };",
+ // Constructor entry includes the initializer's early return.
+ // Its later Store of 2 must not excuse the earlier read of x.
+ "int~ x; int seen; if(arg) return 0; x = 1; new A = { -> seen = x; x = 2; };"
+ } )
+ try {
+ new CodeGen("struct A { "+body+" }; return 0;").driver(CodeGen.Phase.TypeCheck);
+ fail("Read a field before its constructor wrote it");
+ } catch( Parser.ParseException e ) {
+ assertTrue(e.getMessage(),e.getMessage().contains("might not be initialized"));
+ }
+ }
+
+ @Test public void testRequiredNumberBesideNullableDefault() {
+ CodeGen code = new CodeGen("""
+ struct A { int~ x; A? p; new A = { -> x = 10; }; };
+ return new A().x;
+ """).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("10",Eval2.eval(code,0));
+ }
+
+ private void rejects(String fields, String field, boolean explicit) {
+ String src = "struct B {}; struct A { "+fields+" }; return 0;";
+ try {
+ new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
+ fail(src);
+ } catch( Parser.ParseException e ) {
+ assertEquals("'Test.A' is not fully initialized, field '"+field+
+ (explicit ? "' is only partially set in the constructor" :
+ "' needs to be set in a constructor"),e.getMessage());
+ }
+ }
+
+ @Test public void testMissingNumericFields() {
+ for( String type : new String[]{"int", "u8", "f32"} ) {
+ rejects(type+"~ x;","x",false);
+ rejects(type+"~ x; new A = { -> };","x",true);
+ // Converting the merged BOTTOM to the numeric storage type used
+ // to hide the missing assignment from the returned memory.
+ rejects(type+"~ x; new A = { -> if(arg) x = 1; };","x",true);
+ rejects(type+"~ x; new A = { -> if(arg) return 0; x = 1; };","x",true);
+ rejects(type+"~ x; new A = { -> while(arg--) x = 1; };","x",true);
+ }
+ }
+
+ @Test public void testInitializerAndConstructorReturns() {
+ rejects("B b;","b",false);
+ rejects("new A = { -> if(arg) b = new B; }; B b;","b",true);
+ String[] fields = {
+ "int~ x; new A = { -> if(arg) { x = 3; return 0; } x = 7; };",
+ // <init> has no value for x; the user constructor supplies it.
+ "new A = { -> x = 7; }; int~ x;",
+ // The user constructor inherits the declaration's default.
+ "new A = { -> }; int~ x = 7;",
+ "B? b; int x;",
+ };
+ String[][] values = {{"7","3"}, {"7","7"}, {"7","7"}, {"0","0"}};
+ for( int i = 0; i<fields.length; i++ ) {
+ CodeGen code = new CodeGen("struct B {}; struct A { "+fields[i]+" }; return new A().x;")
+ .driver(CodeGen.Phase.TypeCheck);
+ assertEquals(values[i][0],Eval2.eval(code,0));
+ assertEquals(values[i][1],Eval2.eval(code,1));
+ }
+ }
+
+ @Test public void testNonreturningPaths() {
+ // An initializer parsed in dead enclosing code can have TOP memory
+ // rather than a struct-shaped result.
+ new CodeGen("while(false) { struct A { int x; }; } return 0;").driver(CodeGen.Phase.TypeCheck);
+ new CodeGen("struct A { int~ x; new A = { -> while(true) {} }; }; return 0;").driver(CodeGen.Phase.TypeCheck);
+ rejects("int~ x; new A = { -> if(arg) return 0; while(true) {} };","x",true);
+ CodeGen code = new CodeGen("""
+ struct A {
+ int~ x;
+ new A = { -> if(arg) { x = 7; return 0; } while(true) {} };
+ };
+ return new A().x;
+ """).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("7",Eval2.eval(code,1));
+ }
+
+ @Test public void testEarlierConstructorBinding() {
+ // The older binding spelling is parsed before the following field.
+ // Its Return lacks that field, so completion must consult <init> too.
+ rejects("val A = { -> }; B b;","b",true);
+ rejects("int~ x; val A = { -> if(arg) x = 1; };","x",true);
+ new CodeGen("struct B {}; struct A { val A = { -> }; B b = new B; }; return 0;").driver(CodeGen.Phase.TypeCheck);
+ new CodeGen("struct A { val A = { -> while(true) {} }; int~ x; }; return 0;").driver(CodeGen.Phase.TypeCheck);
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/Eval2.java b/src/test/java/com/seaofnodes/simple/Eval2.java
index eea5b023..8d0e3d5b 100644
--- a/src/test/java/com/seaofnodes/simple/Eval2.java
+++ b/src/test/java/com/seaofnodes/simple/Eval2.java
@@ -371,7 +371,7 @@ public abstract class Eval2 {
yield new Object[0];
}
- } else if( Parser.startsClzPrefix(tmp._obj._name) ) {
+ } else if( TypeStruct.startsClzPrefix(tmp._obj._name) ) {
yield classObject(tmp._obj);
} else {
// Constant object with all zeros
diff --git a/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java b/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java
index f583e2d2..66fdacc3 100644
--- a/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java
+++ b/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java
@@ -1,7 +1,7 @@
package com.seaofnodes.simple;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.CodeGen;
-import com.seaofnodes.simple.node.StoreNode;
import org.junit.Test;
import static org.junit.Assert.*;
@@ -19,16 +19,12 @@ public class FinalConstructionTest {
return new C(arg).get();
""";
- @Test public void testFinalConstructorLocals() throws java.io.IOException {
+ @Test public void testFinalConstructorFields() throws java.io.IOException {
var code=new CodeGen(LOOP).driver(CodeGen.Phase.LocalSched);
assertEquals("1",Eval2.eval(code,0));
assertEquals("15",Eval2.eval(code,3));
assertEquals("63",Eval2.eval(code,5));
- code._stop.walk(n -> {
- if( n instanceof StoreNode st && st._name.equals("x") )
- assertEquals("Final field writes belong after the loop",0,st.cfg0().loopDepth());
- return null;
- });
+ // Writes are ordinary Stores and may remain inside the loop.
String[] sources={
"struct C { val x=1; new C={->x=1; x=2;}; }; return new C().x;",
"struct C { int ~x; new C={int n -> self.x=1; for(int i=0;i<n;i++) self.x*=2;}; }; return new C(arg).x;",
diff --git a/src/test/java/com/seaofnodes/simple/ParserTest.java b/src/test/java/com/seaofnodes/simple/ParserTest.java
new file mode 100644
index 00000000..364979f2
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/ParserTest.java
@@ -0,0 +1,33 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.parser.Parser;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class ParserTest {
+ @Test public void testDeferredFragments() {
+ // The for update is scanned before the body, then parsed after it.
+ // Delimiters in strings, characters and comments must not end the scan.
+ String[] updates = {"n=n+\") (\"#", "n=n+')'", "n=n+/* ) */2", "n=n+{ -> 2; }()"};
+ String[] results = {"6", "82", "4", "4"};
+ for( int i = 0; i<updates.length; i++ ) {
+ CodeGen code = new CodeGen("int n=0; for(int i=0; i<2; "+updates[i]+") i++; return n;")
+ .driver(CodeGen.Phase.TypeCheck);
+ assertEquals(updates[i],results[i],Eval2.eval(code,0));
+ }
+ // Declared constructor bodies use the same scanner with brace nesting.
+ CodeGen code = new CodeGen("struct Box { int x; new Box={ -> x=\"}\"#; }; }; return new Box().x;")
+ .driver(CodeGen.Phase.TypeCheck);
+ assertEquals("1",Eval2.eval(code,0));
+ }
+
+ @Test public void testUnclosedDeferredFragment() {
+ try {
+ new CodeGen("for(int i=0; i<2; i++").driver(CodeGen.Phase.TypeCheck);
+ fail("Accepted an unterminated for update");
+ } catch( Parser.ParseException e ) {
+ assertEquals("Unclosed for update",e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/PartialReadTest.java b/src/test/java/com/seaofnodes/simple/PartialReadTest.java
new file mode 100644
index 00000000..a6230b69
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/PartialReadTest.java
@@ -0,0 +1,155 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.node.InitViewNode;
+import com.seaofnodes.simple.node.ConstructNode;
+import com.seaofnodes.simple.parser.Parser;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class PartialReadTest {
+ private static String program(String body, String helper) {
+ return "struct _C { int~ x; int~ y; new _C = { int n -> " + body +
+ " }; }; val _read_noInline = { _C ^p -> " + helper +
+ " }; return new _C(arg).y;";
+ }
+ private static void rejects(String source, String message) {
+ try {
+ new CodeGen(source).driver(CodeGen.Phase.TypeCheck);
+ fail("Accepted " + source);
+ } catch( Parser.ParseException e ) {
+ assertTrue(e.getMessage(),e.getMessage().contains(message));
+ }
+ }
+ @Test public void testLocalRead() {
+ String src = program("x=n; y=_read_noInline(self);", "return p.x;");
+ for( long seed : new long[]{1,123,987654321} ) {
+ CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertNull(code._stop.walk(n -> n instanceof InitViewNode ? n : null));
+ assertEquals("42",Eval2.eval(code,42));
+ }
+ new CodeGen(src).driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION,true,false,0);
+ }
+ @Test public void testRedundantViewsFoldBeforeTypeCheck() {
+ String src=program("x=n; y=0; y=_read_noInline(self);", "return p.x;");
+ CodeGen code=new CodeGen(src).driver(CodeGen.Phase.IterPostOpto);
+ assertNull(code._stop.walk(n -> n instanceof InitViewNode ? n : null));
+ assertNull(code._stop.walk(n -> n instanceof ConstructNode c && !c._partial ? n : null));
+ code.driver(CodeGen.Phase.TypeCheck);
+ assertEquals("31",Eval2.eval(code,31));
+
+ // Here the view still masks y. A valid program need not initialize
+ // every field before lending self, so survival is not itself an error.
+ code=new CodeGen(program("x=n; y=_read_noInline(self);", "return p.x;"))
+ .driver(CodeGen.Phase.IterPostOpto);
+ assertNotNull(code._stop.walk(n -> n instanceof InitViewNode ? n : null));
+ code.driver(CodeGen.Phase.TypeCheck);
+ assertEquals("31",Eval2.eval(code,31));
+ }
+ @Test public void testInliningKeepsBorrowRestriction() {
+ rejects("""
+ struct _C {
+ int !x;
+ new _C={ int n -> x=n; if(!n) _write(self); };
+ };
+ val _write={ !_C ^p -> if(p.x) p.x=9; return p.x; };
+ val a=new _C(0); val b=new _C(1);
+ return _write(b)+a.x;
+ ""","incomplete object");
+ }
+ @Test public void testMissingAndBranchField() {
+ for( String use : new String[]{"p.x", "p.x*0", "p.x==p.x"} )
+ rejects(program("y=_read_noInline(self); x=n;", "return "+use+";"),"might not be initialized");
+ rejects(program("if(n) x=n; y=_read_noInline(self); x=0;", "return p.x;"),"might not be initialized");
+ }
+ @Test public void testActualValueFromMemory() {
+ for( String body : new String[]{"x=1; val p=self; x=2; y=_read_noInline(p);",
+ "val p=self; x=2; y=_read_noInline(p);"} ) {
+ CodeGen code = new CodeGen(program(body,"return p.x;")).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("2",Eval2.eval(code,0));
+ }
+ }
+ @Test public void testBorrowedWrite() {
+ rejects(program("x=n; y=_read_noInline(self);", "p.x=9; return p.x;")
+ .replace("int~ x", "int !x").replace("_C ^p", "!_C ^p"),"incomplete object");
+ }
+ @Test public void testUnknownCallerCannotRead() {
+ rejects(program("x=n; y=_read_noInline(self);", "return p.x;")
+ .replace("_read_noInline","read_noInline"),"might not be initialized");
+ }
+ @Test public void testForwardingAndMergedCallers() {
+ String src = program("x=n; y=_read_noInline(self);", "return _next_noInline(p);")
+ + " val _next_noInline = { _C ^p -> return p.x; };";
+ assertEquals("17",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),17));
+ rejects(program("y=_read_noInline(self); x=n; y=_read_noInline(self);", "return p.x;"),"might not be initialized");
+ }
+ @Test public void testLoops() {
+ String src = program("x=1; for(int i=0; i<n; i++) x+=2; y=_read_noInline(self);", "return p.x;");
+ for( long seed : new long[]{1,123,987654321} ) {
+ CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("7",Eval2.eval(code,3));
+ assertEquals("1",Eval2.eval(code,0));
+ }
+ rejects(program("for(int i=0; i<n; i++) x=n; y=_read_noInline(self); x=0;", "return p.x;"),"might not be initialized");
+ }
+ @Test public void testNoPublication() {
+ rejects("struct Box { Test._C? !p; }; val box=new Box; " +
+ program("x=n; y=_read_noInline(self);", "box.p=p; return p.x;"),"ordinary object");
+ }
+ @Test public void testConstructorReadAndRetention() {
+ String src = """
+ struct _Child {
+ Test._Parent parent;
+ int~ seen;
+ new _Child = { Test._Parent ^p -> parent=p; seen=p.x; };
+ };
+ struct _Parent {
+ int~ x;
+ _Child child;
+ new _Parent = { int n -> x=n; child=new _Child(self); };
+ };
+ val p=new _Parent(arg); return p.child.seen+p.child.parent.x;
+ """;
+ assertEquals("22",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),11));
+ rejects(src.replace("x=n; child=new _Child(self);","child=new _Child(self); x=n;"),"might not be initialized");
+ }
+ @Test public void testInlineReadAndWrite() {
+ String src=program("x=n; y=_read_noInline(self);","return p.x;").replace("_noInline","");
+ assertEquals("19",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),19));
+ rejects(src.replace("return p.x;","p.x=9; return p.x;").replace("int~ x","int !x").replace("_C ^p","!_C ^p"),"incomplete object");
+ rejects(src.replace("x=n; y=", "y=").replace("(self);", "(self); x=n;"),"might not be initialized");
+ }
+ @Test public void testArrayAndNestedReference() {
+ String src="""
+ struct _C {
+ int[] data;
+ int~ y;
+ new _C = { int n -> data=new int[1]; data[0]=n; y=_read_noInline(self); };
+ };
+ val _read_noInline={ _C ^p -> return p.data[0]; };
+ return new _C(arg).y;
+ """;
+ assertEquals("23",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),23));
+ // A set reference slot is not proof that its referent is initialized.
+ rejects("struct D { int x=7; }; "+src.replace("int[] data","D data")
+ .replace("data=new int[1]; data[0]=n;","data=new D;")
+ .replace("p.data[0]","p.data.x"),"might not be initialized");
+ }
+ @Test public void testCompleteBorrowCannotPublishPartial() {
+ rejects("""
+ struct _C { int x=7; };
+ struct Box { _C? p; }; val box=new Box;
+ val _store={ !_C ^dst, _C ^src -> box.p=src; return dst.x; };
+ return _store(new _C,new _C);
+ ""","ordinary object");
+ }
+ @Test public void testSameClassMemory() {
+ String src=program("x=n; y=n ? _read_noInline(self) : x;",
+ "val other=new _C(0); return p.x+other.y;");
+ for( long seed : new long[]{1,123,987654321} ) {
+ CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("29",Eval2.eval(code,29));
+ assertEquals("0",Eval2.eval(code,0));
+ }
+ }
+}
diff --git a/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java b/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java
index fa14fc2c..88c11101 100644
--- a/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java
+++ b/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java
@@ -1,5 +1,6 @@
package com.seaofnodes.simple;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.codegen.*;
import com.seaofnodes.simple.node.*;
import com.seaofnodes.simple.type.TypeInteger;
diff --git a/src/test/java/com/seaofnodes/simple/TypeTest.java b/src/test/java/com/seaofnodes/simple/TypeTest.java
index b4055eef..16b75fdc 100644
--- a/src/test/java/com/seaofnodes/simple/TypeTest.java
+++ b/src/test/java/com/seaofnodes/simple/TypeTest.java
@@ -7,6 +7,26 @@ import static org.junit.Assert.assertSame;
public class TypeTest {
+ @Test public void testOpaqueExternalEscapes() {
+ TypeMem defaults = TypeMem.START.makeFrom(XInt.make(2),XInt.make(3));
+ TypeMem prior = TypeMem.START.makeFrom(XInt.make(5),XInt.make(7));
+ TypeMemPtr opaque = TypeMemPtr.makeNullable(TypeStruct.forward("OpaqueExternal"));
+ TypeMem escaped = prior.escapesFrom(opaque,defaults);
+ assertSame(XInt.meet(prior._escFs,defaults._escFs),escaped._escFs);
+ assertSame(XInt.meet(prior._escAs,defaults._escAs),escaped._escAs);
+ // Before type closure the same name might still resolve locally.
+ assertSame(XInt.FULL,prior.escapesFrom(opaque,TypeMem.BOT)._escFs);
+ for( Type unknown : new Type[]{Type.BOTTOM,TypeScalar.BOT,TypePtr.PTR,
+ TypeMemPtr.make(TypeStruct.open("LocalStillOpen"))} ) {
+ TypeMem broad = prior.escapesFrom(unknown,defaults);
+ assertSame(XInt.FULL,broad._escFs);
+ assertSame(XInt.FULL,broad._escAs);
+ }
+ // Concrete private callbacks must not be restricted to public names.
+ TypeFunPtr callback = TypeFunPtr.make1((byte)2,true,TypeFunPtr.TEMPTY,TypeInteger.BOT,11);
+ assertSame(XInt.make(11),TypeMem.START.escapesFrom(callback,defaults)._escFs);
+ }
+
// Aggregate finality is carried by its fields. Canonicalizing the unused
// aggregate bit must commute with dual, even for named (non-high) structs.
@Test public void testAggregateMemoryDual() {
@@ -147,7 +167,7 @@ public class TypeTest {
Assert.assertNotEquals(ptr1, ptr2);
Assert.assertEquals(ptr1, ptr1.dual().dual());
- Assert.assertEquals(TypeMemPtr.makeNullable(TypeStruct.BOT).withAccess(false), ptr1.meet(ptr2nil));
+ Assert.assertEquals(TypeMemPtr.makeNullable(TypeStruct.BOT).withAccess(false).withPartial(false), ptr1.meet(ptr2nil));
TypeMemPtr TOP = TypeMemPtr.TOP;
TypeMemPtr BOT = TypeMemPtr.makeNullable(TypeStruct.BOT);
@@ -156,7 +176,7 @@ public class TypeTest {
Type PTR_meet_NULL = NULL.meet(PTR);
Assert.assertEquals(BOT, PTR_meet_NULL);
Type ptr1_meet_ptr2 = ptr1.meet(ptr2);
- Assert.assertEquals(PTR.withAccess(false), ptr1_meet_ptr2);
+ Assert.assertEquals(PTR.withAccess(false).withPartial(false), ptr1_meet_ptr2);
Type NULL_join_ptr1 = NULL.join(ptr1);
Assert.assertEquals(TypePtr.XNPTR, NULL_join_ptr1);
Type NULL_join_PTR = PTR.join(NULL);
diff --git a/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java b/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java
index 292f1ae7..adbb10a5 100644
--- a/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java
+++ b/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java
@@ -1,6 +1,7 @@
package com.seaofnodes.simple.codegen;
import com.seaofnodes.simple.node.*;
+import com.seaofnodes.simple.node.cpus.x86_64_v2.SubX86;
import com.seaofnodes.simple.node.cpus.x86_64_v2.x86_64_v2;
import com.seaofnodes.simple.type.TypeInteger;
import static org.junit.Assert.*;
@@ -124,6 +125,30 @@ public class RegAllocTestSupport {
assertTrue(BuildLRG.run(0,new RegAlloc(code)));
}
+ public static void commutativeLoop() {
+ for( String op : new String[]{"+","*","^","&","|"} )
+ for( boolean reversed : new boolean[]{false,true} ) {
+ String src = "return { u8[] data -> int h = 1; for(int i = 0; i < data#; i++) " +
+ "h = ((h<<5)-h) "+op+" data[i]; return h; };";
+ CodeGen code = new CodeGen(src).driver(CodeGen.Phase.LocalSched,"x86_64_v2","SystemV");
+ // Exercise both destructive operands regardless of ideal-node
+ // numbering. The shifted value overlaps the old loop hash.
+ int found = 0;
+ for( CFGNode bb : code._cfg )
+ for( Node n : bb.outs() )
+ if( n instanceof MachNode mach && mach.commutes() && mach.twoAddress()==1 &&
+ (n.in(1) instanceof SubX86 || n.in(2) instanceof SubX86) ) {
+ if( (n.in(1) instanceof SubX86)==reversed ) n.swap12();
+ found++;
+ }
+ assertEquals(op,1,found);
+ code.driver(CodeGen.Phase.RegAlloc,"x86_64_v2","SystemV");
+ checkRegisters(code);
+ assertEquals(op+" reversed="+reversed,8,code._regAlloc._spillScaled);
+ code.encode();
+ }
+ }
+
private static class CallOp extends CallNode implements MachNode {
CallOp(Node ctrl, Node def) { super(null,ctrl,null,def,def); }
@Override public RegMask regmap(int i) { return i==2 ? A : i==3 ? B : null; }
diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java b/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java
index 55c2c040..73ba6afd 100644
--- a/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java
+++ b/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java
@@ -1,6 +1,6 @@
package com.seaofnodes.simple.fuzzer;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.Eval2;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.node.Node;
diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java b/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java
index 9fe2e405..c38c423d 100644
--- a/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java
+++ b/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java
@@ -1,6 +1,6 @@
package com.seaofnodes.simple.fuzzer;
-import com.seaofnodes.simple.IterPeeps;
+import com.seaofnodes.simple.codegen.IterPeeps;
import com.seaofnodes.simple.codegen.CodeGen;
import com.seaofnodes.simple.node.Node;
import com.seaofnodes.simple.node.StopNode;
diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java b/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java
index c5e94047..78fb8e44 100644
--- a/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java
+++ b/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java
@@ -1,6 +1,6 @@
package com.seaofnodes.simple.fuzzer;
-import com.seaofnodes.simple.Parser;
+import com.seaofnodes.simple.parser.Parser;
import com.seaofnodes.simple.node.ScopeNode;
import com.seaofnodes.simple.type.TypeInteger;
import java.util.ArrayList;
@@ -8,7 +8,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.function.Predicate;
-import static com.seaofnodes.simple.Parser.KEYWORDS;
+import static com.seaofnodes.simple.parser.Parser.KEYWORDS;
/**